diff --git a/code/KM_Render_Mobile.pas b/code/KM_Render_Mobile.pas index bc5cbc6..b442a8d 100644 --- a/code/KM_Render_Mobile.pas +++ b/code/KM_Render_Mobile.pas @@ -14,16 +14,174 @@ function KMRenderMobileCreateTexture(aMinFilter, aMagFilter: TKMFilterType): Car procedure KMRenderMobileDeleteTexture(aTexId: Cardinal); procedure KMRenderMobileUpdateTexture(aTexture: Cardinal; aWidth, aHeight: Word; aMode: TKMTexFormat; const aData: Pointer); procedure KMRenderMobileInvalidateTextureBindingCache; +procedure KMRenderMobileApplyOrdinary2DProjection(aScreenX, aScreenY: Word); +procedure KMRenderMobileClearCurrentTarget; +procedure KMRenderMobileSetActiveTextureUnit0; +procedure KMRenderMobileDrawSampledTriangles( + const aPositionData, aTexCoordData: Pointer; + aStride, aVertexCount: Integer; + aColorR, aColorG, aColorB, aColorA: Single); +procedure KMRenderMobileActivateSampledProgram; +procedure KMRenderMobileInvalidateSampledProgram; +procedure KMRenderMobileInitializeSampledProgram; +function KMRenderMobileSampledProgramReady: Boolean; +procedure KMRenderMobileDrawBrightnessOverlay( + aIntensity: Single; aWidth, aHeight: Word); implementation uses - SysUtils, CastleGLUtils, CastleGLES; + SysUtils, CastleGLUtils, CastleGLES, CastleRectangles, CastleProjection, CastleRenderContext, CastleVectors, + CastleGLShaders, CastleRenderOptions; var gTextureBackendReady: Boolean; gTextureBindingKnown: Boolean; gCachedTextureId: Cardinal; + gSampledProgram: TGLSLProgram; + +const + SampledVertexShader = + 'attribute vec3 aPosition;' + #10 + + 'attribute vec2 aTexCoord;' + #10 + + 'uniform mat4 uProjection;' + #10 + + 'varying vec2 vTexCoord;' + #10 + + 'void main() {' + #10 + + ' gl_Position = uProjection * vec4(aPosition, 1.0);' + #10 + + ' vTexCoord = aTexCoord;' + #10 + + '}'; + SampledFragmentShader = + 'uniform sampler2D uTexture;' + #10 + + 'uniform vec4 uColor;' + #10 + + 'varying vec2 vTexCoord;' + #10 + + 'void main() {' + #10 + + ' gl_FragColor = texture2D(uTexture, vTexCoord) * uColor;' + #10 + + '}'; + + +procedure KMRenderMobileApplyOrdinary2DProjection(aScreenX, aScreenY: Word); +var + Dimensions: TFloatRectangle; +begin + Dimensions.Left := 0; + Dimensions.Width := aScreenX; + Dimensions.Bottom := aScreenY; + Dimensions.Height := -Single(aScreenY); + RenderContext.ProjectionMatrix := OrthoProjectionMatrix(Dimensions, -100, 100); +end; + + +procedure KMRenderMobileClearCurrentTarget; +begin + RenderContext.Clear([cbColor, cbDepth, cbStencil], TVector4.Zero); +end; + + +procedure KMRenderMobileDrawBrightnessOverlay( + aIntensity: Single; aWidth, aHeight: Word); +begin + DrawRectangle( + Rectangle(0, 0, aWidth, aHeight), + Vector4(aIntensity, aIntensity, aIntensity, aIntensity), + bsDstColor, bdOne, True); + RenderContext.BlendingEnable(bsSrcAlpha, bdOneMinusSrcAlpha); +end; + + +procedure KMRenderMobileSetActiveTextureUnit0; +begin + CastleGLES.glActiveTexture(CastleGLES.GL_TEXTURE0); +end; + + +procedure KMRenderMobileInitializeSampledProgram; +var + candidate: TGLSLProgram; + positionAttribute, texCoordAttribute: TGLSLAttribute; + projectionUniform, textureUniform, colorUniform: TGLSLUniform; +begin + if gSampledProgram <> nil then Exit; + + candidate := TGLSLProgram.Create; + try + candidate.FragmentShaderRequired := True; + candidate.AttachVertexShader(SampledVertexShader); + candidate.AttachFragmentShader(SampledFragmentShader); + candidate.Link; + positionAttribute := candidate.Attribute('aPosition'); + texCoordAttribute := candidate.Attribute('aTexCoord'); + projectionUniform := candidate.Uniform('uProjection', umIgnore); + textureUniform := candidate.Uniform('uTexture', umIgnore); + colorUniform := candidate.Uniform('uColor', umIgnore); + if (projectionUniform.Location = -1) or (textureUniform.Location = -1) + or (colorUniform.Location = -1) then + raise Exception.Create('Sampled shader is missing a required uniform'); + gSampledProgram := candidate; + candidate := nil; + finally + candidate.Free; + end; +end; + + +procedure KMRenderMobileActivateSampledProgram; +begin + if gSampledProgram = nil then + raise Exception.Create('Sampled shader program is not ready'); + gSampledProgram.Enable; +end; + + +procedure KMRenderMobileInvalidateSampledProgram; +begin + FreeAndNil(gSampledProgram); +end; + + +function KMRenderMobileSampledProgramReady: Boolean; +begin + Result := gSampledProgram <> nil; +end; + + +procedure KMRenderMobileDrawSampledTriangles( + const aPositionData, aTexCoordData: Pointer; + aStride, aVertexCount: Integer; + aColorR, aColorG, aColorB, aColorA: Single); +var + positionAttribute, texCoordAttribute: TGLSLAttribute; + projectionUniform, colorUniform: TGLSLUniform; + positionEnabled, texCoordEnabled: Boolean; +begin + if (aPositionData = nil) or (aTexCoordData = nil) or (aStride <= 0) or (aVertexCount <= 0) then + raise Exception.Create('Invalid sampled triangle draw input'); + if gSampledProgram = nil then + KMRenderMobileActivateSampledProgram; + + positionAttribute := gSampledProgram.Attribute('aPosition'); + texCoordAttribute := gSampledProgram.Attribute('aTexCoord'); + projectionUniform := gSampledProgram.Uniform('uProjection', umIgnore); + colorUniform := gSampledProgram.Uniform('uColor', umIgnore); + if (projectionUniform.Location = -1) or (colorUniform.Location = -1) then + raise Exception.Create('Sampled shader is missing a required draw uniform'); + + KMRenderMobileActivateSampledProgram; + projectionUniform.SetValue(RenderContext.ProjectionMatrix); + colorUniform.SetValue(Vector4(aColorR, aColorG, aColorB, aColorA)); + RenderContext.BindBuffer[btArray] := 0; + positionEnabled := False; + texCoordEnabled := False; + try + positionAttribute.EnableArrayVector3(nil, CastleGLES.GLsizei(aStride), PtrUInt(aPositionData)); + positionEnabled := True; + texCoordAttribute.EnableArrayVector2(nil, CastleGLES.GLsizei(aStride), PtrUInt(aTexCoordData)); + texCoordEnabled := True; + CastleGLES.glDrawArrays(CastleGLES.GL_TRIANGLES, 0, CastleGLES.GLsizei(aVertexCount)); + finally + if texCoordEnabled then texCoordAttribute.DisableArray; + if positionEnabled then positionAttribute.DisableArray; + end; +end; procedure RequireTextureBackendReady; diff --git a/config/extra-paths.json b/config/extra-paths.json index 6fb38b3..4dee6da 100755 --- a/config/extra-paths.json +++ b/config/extra-paths.json @@ -25,7 +25,10 @@ "src/res/KM_Resource.pas", "src/common/KM_Defaults.pas", "src/res/KM_ResTypes.pas", - "src/utils/KromOGLUtils.pas" + "src/utils/KromOGLUtils.pas", + "src/render/KM_RenderPool.pas", + "src/gui/KM_InterfaceGame.pas", + "src/render/KM_RenderTerrain.pas" ], "source_paths": [ "code/resource" diff --git a/port-source/kam_remake/src/KM_GameApp.pas b/port-source/kam_remake/src/KM_GameApp.pas index 76ef538..4ff55a1 100644 --- a/port-source/kam_remake/src/KM_GameApp.pas +++ b/port-source/kam_remake/src/KM_GameApp.pas @@ -310,6 +310,8 @@ begin fApplicationGraphicsReadyGeneration := 0; if not TKMRender.TextureBackendReady then Exit; + if not TKMRender.SampledProgramReady then + Exit; if gGame <> nil then begin if gRes = nil then diff --git a/port-source/kam_remake/src/common/KM_Defaults.pas b/port-source/kam_remake/src/common/KM_Defaults.pas index bfe1973..5692f75 100644 --- a/port-source/kam_remake/src/common/KM_Defaults.pas +++ b/port-source/kam_remake/src/common/KM_Defaults.pas @@ -185,7 +185,6 @@ var SHOW_TERRAIN_PASS :Byte = 0; //Byte(TKMTerrainPassability) SHOW_TILES_OWNER :Boolean = False; //Show tiles owner SHOW_ATTACK_RADIUS :Boolean = False; //Render towers/archers attack radius - RENDER_3D :Boolean = False; //Experimental 3D render {Render} SAVE_MAP_TO_FBO_RENDER :Boolean = False; //Do Render is performed into FBO off-screen buffer? {Stats} diff --git a/port-source/kam_remake/src/gui/KM_InterfaceGame.pas b/port-source/kam_remake/src/gui/KM_InterfaceGame.pas new file mode 100644 index 0000000..8ef4b21 --- /dev/null +++ b/port-source/kam_remake/src/gui/KM_InterfaceGame.pas @@ -0,0 +1,707 @@ +unit KM_InterfaceGame; +{$I KaM_Remake.inc} +interface +uses + {$IFDEF MSWindows} Windows, {$ENDIF} + {$IFDEF FPC} + {$IFDEF Unix} LCLIntF, {$ENDIF} + {$ENDIF} + SysUtils, Classes, Math, Controls, + KM_Defaults, + KM_Controls, KM_ControlsBase, + KM_Points, + KM_InterfaceDefaults, KM_CommonTypes, KM_Render, KM_MinimapGame, KM_Viewport, KM_ResFonts, + KM_ResTypes, KM_CommonClassesExt, KM_AITypes; + + +type + // Common class for ingame interfaces (Gameplay, MapEd) + TKMUserInterfaceGame = class abstract(TKMUserInterfaceCommon) + private + fDragScrollingCursorPos: TPoint; + fDragScrollingViewportPos: TKMPointF; + fOnUserAction: TKMUserActionEvent; + + fLogStringList: TKMLimitedList; + + function GetLogString(aLimit: Integer): string; + procedure LogMessageHappened(const aLogMessage: UnicodeString); + + procedure ResetDragScrolling; + procedure PaintDefences; + protected + fMinimap: TKMMinimapGame; + fViewport: TKMViewport; + fDragScrolling: Boolean; + + fPaintDefences: Boolean; + + Bevel_DebugInfo: TKMBevel; + Label_DebugInfo: TKMLabel; + + function ZoomChangeBlocked: Boolean; virtual; + + function IsDragScrollingAllowed: Boolean; virtual; + function GetHintPositionBase: TKMPoint; override; + function GetHintFont: TKMFont; override; + function GetHintKind: TKMHintKind; override; + + function GetDebugInfo: string; virtual; + + procedure InitDebugControls; + + procedure ViewportPositionChanged(const aPos: TKMPointF); + procedure OptionsChanged; virtual; abstract; + + procedure HandleScrollKeysDown(Key: Word; var aHandled: Boolean); + procedure HandleScrollKeysUp(Key: Word; var aHandled: Boolean); + public + constructor Create(aRender: TKMRender); reintroduce; + destructor Destroy; override; + + property Minimap: TKMMinimapGame read fMinimap; + property Viewport: TKMViewport read fViewport; + property OnUserAction: TKMUserActionEvent read fOnUserAction write fOnUserAction; + + function CursorToMapCoord(X, Y: Integer): TKMPointF; + + procedure DebugControlsUpdated(aSenderTag: Integer); override; + + procedure KeyDown(Key: Word; Shift: TShiftState; aIsFirst: Boolean; var aHandled: Boolean); override; + procedure KeyUp(Key: Word; Shift: TShiftState; var aHandled: Boolean); override; + procedure KeyPress(Key: Char); override; + procedure MouseWheel(Shift: TShiftState; WheelSteps: Integer; X,Y: Integer; var aHandled: Boolean); override; + procedure MouseMove(Shift: TShiftState; X,Y: Integer; var aHandled: Boolean); override; + procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X,Y: Integer); override; + procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X,Y: Integer); override; + + procedure ShowDebugInfo; + + procedure GameSpeedChanged(aFromSpeed, aToSpeed: Single); + procedure SyncUI(aMoveViewport: Boolean = True); virtual; + procedure SyncUIView(const aCenter: TKMPointF); overload; + procedure SyncUIView(const aCenter: TKMPointF; aZoom: Single); overload; + procedure UpdateGameCursor(X, Y: Integer; Shift: TShiftState); + procedure UpdateStateIdle(aFrameTime: Cardinal); virtual; abstract; + procedure UpdateState(aGlobalTickCount: Cardinal); override; + + procedure Paint; override; + end; + + +const + // Toolbar pads + TB_PAD = 9; // Picked up empirically + TB_WIDTH = 180; // Max width of sidebar elements + TB_MAP_ED_WIDTH = MAPED_TOOLBAR_WIDTH - 44; //Max width of sidebar elements in Map Editor + PAGE_TITLE_Y = 5; // Page title offset + TERRAIN_PAGE_TITLE_Y = PAGE_TITLE_Y + 2; // Terrain pages title offset + STATS_LINES_CNT = 13; //Number of stats (F3) lines + + DEFENCE_LINE_TYPE_COL: array [TKMAIDefencePosType] of Cardinal = ($FF80FF00, $FFFF8000); + + // Shortcuts + // All shortcuts are in English and are the same for all languages to avoid + // naming collisions and confusion in discussions + + GUI_HOUSE_COUNT = 28; // Number of KaM houses to show in GUI + GUIHouseOrder: array [1..GUI_HOUSE_COUNT] of TKMHouseType = ( + htSchool, htInn, htQuarry, htWoodcutters, htSawmill, + htFarm, htMill, htBakery, htSwine, htButchers, + htVineyard, htGoldMine, htCoalMine, htMetallurgists, htWeaponWorkshop, + htTannery, htArmorWorkshop, htStables, htIronMine, htIronSmithy, + htWeaponSmithy, htArmorSmithy, htBarracks, htStore, htWatchTower, + htFishermans, htMarket, htTownHall); + + // Template for how resources are shown in Barracks + BARRACKS_RES_COUNT = 11; + BarracksResType: array [1..BARRACKS_RES_COUNT] of TKMWareType = + (wtWoodenShield, wtIronShield, wtLeatherArmor, wtIronArmor, wtAxe, wtSword, + wtLance, wtPike, wtBow, wtCrossbow, wtHorse); + + // Layout of resources in Store + STORE_RES_COUNT = 28; + StoreResType: array [1..STORE_RES_COUNT] of TKMWareType = + (wtTrunk, wtStone, wtTimber, wtIronOre, wtGoldOre, + wtCoal, wtIron, wtGold, wtWine, wtCorn, + wtBread, wtFlour, wtLeather, wtSausage, wtPig, + wtSkin, wtWoodenShield, wtIronShield, wtLeatherArmor, wtIronArmor, + wtAxe, wtSword, wtLance, wtPike, wtBow, + wtCrossbow, wtHorse, wtFish); + + School_Order: array [0..13] of TKMUnitType = ( + utSerf, utBuilder, utStonemason, utWoodcutter, utCarpenter, + utFisher, utFarmer, utBaker, utAnimalBreeder, utButcher, + utMiner, utMetallurgist, utSmith, utRecruit); + + Barracks_Order: array [0..8] of TKMUnitType = ( + utMilitia, utAxeFighter, utSwordFighter, utBowman, utCrossbowman, + utLanceCarrier, utPikeman, utScout, utKnight); + + TownHall_Order: array [0..4] of TKMUnitType = ( + utRebel, utRogue, utVagabond, utBarbarian, utWarrior); + + Soldiers_Order: array[0..13] of TKMUnitType = ( + utMilitia, utAxeFighter, utSwordFighter, utBowman, utCrossbowman, + utLanceCarrier, utPikeman, utScout, utKnight, + utRebel, utRogue, utVagabond, utBarbarian, utWarrior); + + // Stats get stacked by UI logic (so that on taller screens they all were + // in nice pairs, and would stack up only on short screens) + StatPlan: array [0..STATS_LINES_CNT-1] of record + HouseType: array [0..3] of TKMHouseType; + UnitType: array [0..1] of TKMUnitType; + end = ( + (HouseType: (htQuarry, htNone, htNone, htNone); UnitType: (utStonemason, utNone)), + (HouseType: (htWoodcutters, htNone, htNone, htNone); UnitType: (utWoodcutter, utNone)), + (HouseType: (htFishermans, htNone, htNone, htNone); UnitType: (utFisher, utNone)), + (HouseType: (htFarm, htVineyard, htNone, htNone); UnitType: (utFarmer, utNone)), + (HouseType: (htMill, htBakery, htNone, htNone); UnitType: (utBaker, utNone)), + (HouseType: (htSwine, htStables, htNone, htNone); UnitType: (utAnimalBreeder, utNone)), + (HouseType: (htButchers, htTannery, htNone, htNone); UnitType: (utButcher, utNone)), + (HouseType: (htMetallurgists, htIronSmithy, htNone, htNone); UnitType: (utMetallurgist, utNone)), + (HouseType: (htArmorSmithy, htWeaponSmithy, htNone, htNone); UnitType: (utSmith, utNone)), + (HouseType: (htCoalMine, htIronMine, htGoldMine, htNone); UnitType: (utMiner, utNone)), + (HouseType: (htSawmill, htWeaponWorkshop, htArmorWorkshop, htNone); UnitType: (utCarpenter, utNone)), + (HouseType: (htBarracks, htTownHall, htWatchTower, htNone); UnitType: (utRecruit, utNone)), + (HouseType: (htStore, htSchool, htInn, htMarket); UnitType: (utSerf, utBuilder)) + ); + + MapEd_Order: array [0..13] of TKMUnitType = ( + utMilitia, utAxeFighter, utSwordFighter, utBowman, utCrossbowman, + utLanceCarrier, utPikeman, utScout, utKnight, utBarbarian, + utRebel, utRogue, utWarrior, utVagabond); + + MapEd_Icon: array [0..13] of Word = ( + 61, 62, 63, 64, 65, + 66, 67, 68, 69, 70, + 79, 80, 81, 82); + + Animal_Order: array [0..7] of TKMUnitType = ( + utWolf, utFish, utWatersnake, utSeastar, + utCrab, utWaterflower, utWaterleaf, utDuck); + + Animal_Icon: array [0..7] of Word = ( + 71, 72, 73, 74, + 75, 76, 77, 78); + + MARKET_RES_HEIGHT = 35; + + // Big tab buttons in MapEd + BIG_TAB_W = 41; + BIG_PAD_W = 41; + BIG_TAB_H = 36; + // Small sub-tab buttons in MapEd + SMALL_TAB_W = 30; + SMALL_PAD_W = 30; + SMALL_TAB_H = 26; + + MESSAGE_AREA_HEIGHT = 173+17; // Image_ChatHead + Image_ChatBody + MESSAGE_AREA_RESIZE_Y = 200; // How much can we resize it + + +implementation +uses + StrUtils, KromUtils, + KM_AIDefensePos, + KM_Cursor, KM_Main, KM_System, + KM_GameParams, KM_GameSettings, + KM_HandsCollection, + KM_Terrain, + KM_RenderPool, KM_RenderUI, KM_Pics, + KM_Resource, KM_ResKeys, + KM_Sound, KM_ScriptSound, + KM_CommonUtils, KM_Log; + + +{ TKMUserInterfaceGame } +constructor TKMUserInterfaceGame.Create(aRender: TKMRender); +begin + inherited Create(aRender.ScreenX, aRender.ScreenY); + + fMinimap := TKMMinimapGame.Create(False); + fViewport := TKMViewport.Create(GetToolbarWidth, aRender.ScreenX, aRender.ScreenY, ViewportPositionChanged); + + gLog.AddOnLogEventSub(LogMessageHappened); + fLogStringList := TKMLimitedList.Create(80); // 80 lines max + + fDragScrolling := False; + fDragScrollingCursorPos.X := 0; + fDragScrollingCursorPos.Y := 0; + fDragScrollingViewportPos := KMPOINTF_ZERO; + + fPaintDefences := False; + + gRenderPool := TKMRenderPool.Create(fViewport); +end; + + +destructor TKMUserInterfaceGame.Destroy; +begin + gLog.RemoveOnLogEventSub(LogMessageHappened); + fLogStringList.Free; + + FreeAndNil(fMinimap); + FreeAndNil(fViewport); + FreeAndNil(gRenderPool); + inherited; +end; + + +procedure TKMUserInterfaceGame.DebugControlsUpdated(aSenderTag: Integer); +begin + inherited; + + if aSenderTag = Ord(dcFlatTerrain) then + gTerrain.UpdateLighting; +end; + + +function TKMUserInterfaceGame.GetHintPositionBase: TKMPoint; +begin + Result := TKMPoint.New(GetToolbarWidth + 35, Panel_Main.Height); +end; + + +function TKMUserInterfaceGame.GetHintFont: TKMFont; +begin + Result := fntOutline; +end; + + +function TKMUserInterfaceGame.GetHintKind: TKMHintKind; +begin + Result := hkStatic; +end; + + +procedure TKMUserInterfaceGame.LogMessageHappened(const aLogMessage: UnicodeString); +begin + if Self = nil then Exit; + + if SHOW_LOG_IN_GUI or UPDATE_LOG_FOR_GUI then + fLogStringList.Add(ReplaceStr(DeleteDoubleSpaces(aLogMessage), EolW, '|')); +end; + + + +function TKMUserInterfaceGame.GetLogString(aLimit: Integer): string; +var + I: Integer; +begin + Result := ''; + // attach from the end, the most fresh log lines, till the limit + for I := fLogStringList.Count - 1 downto Max(fLogStringList.Count - aLimit - 1, 0) do + Result := fLogStringList[I] + '|' + Result; +end; + + +function TKMUserInterfaceGame.GetDebugInfo: string; +begin + Result :=''; + + if SHOW_VIEWPORT_INFO then + Result := Result + 'Viewport: ' + fViewport.ToStr + '|'; + + if SHOW_FPS then + Result := Result + gMain.FPSString + '|'; +end; + + +procedure TKMUserInterfaceGame.ShowDebugInfo; +const + BEVEL_PAD = 20; +var + S: string; + linesCnt: Integer; + textSize: TKMPoint; +begin + S := GetDebugInfo; + + // Add logs at the end + if SHOW_LOG_IN_GUI then + begin + linesCnt := (Bevel_DebugInfo.Parent.Height + - Bevel_DebugInfo.Top + - BEVEL_PAD + - gRes.Fonts[Label_DebugInfo.Font].GetTextSize(S).Y) + div gRes.Fonts[Label_DebugInfo.Font].LineHeight; + S := S + '|Logs:|' + GetLogString(linesCnt - 1); + end; + + Label_DebugInfo.Caption := S; + Label_DebugInfo.Visible := (Trim(S) <> ''); + + Assert(InRange(DEBUG_TEXT_FONT_ID, Ord(Low(TKMFont)), Ord(High(TKMFont)))); + Label_DebugInfo.Font := TKMFont(DEBUG_TEXT_FONT_ID); + + textSize := gRes.Fonts[Label_DebugInfo.Font].GetTextSize(S); + + Bevel_DebugInfo.Width := IfThen(textSize.X <= 1, 0, textSize.X + BEVEL_PAD); + Bevel_DebugInfo.Height := IfThen(textSize.Y <= 1, 0, textSize.Y + BEVEL_PAD); + + Bevel_DebugInfo.Visible := SHOW_DEBUG_OVERLAY_BEVEL and (Trim(S) <> '') ; +end; + + +procedure TKMUserInterfaceGame.ViewportPositionChanged(const aPos: TKMPointF); +begin + gSoundPlayer.UpdateListener(aPos.X, aPos.Y); + if gScriptSounds <> nil then + gScriptSounds.UpdateListener(aPos.X, aPos.Y); +end; + + +procedure TKMUserInterfaceGame.KeyPress(Key: Char); +begin + if Assigned(fOnUserAction) then + fOnUserAction(uatKeyPress); + + inherited; +end; + + +procedure TKMUserInterfaceGame.HandleScrollKeysDown(Key: Word; var aHandled: Boolean); +{$IFDEF MSWindows} +var + windowRect: TRect; +{$ENDIF} +begin + aHandled := True; + + if gMySpectator.Hand.InCinematic then + begin + aHandled := False; + Exit; + end; + + //Scrolling + if Key = gResKeys[kfScrollLeft] then + fViewport.ScrollKeyLeft := True + else if Key = gResKeys[kfScrollRight] then + fViewport.ScrollKeyRight := True + else if Key = gResKeys[kfScrollUp] then + fViewport.ScrollKeyUp := True + else if Key = gResKeys[kfScrollDown] then + fViewport.ScrollKeyDown := True + else if Key = gResKeys[kfZoomIn] then + fViewport.ZoomKeyIn := True + else if Key = gResKeys[kfZoomOut] then + fViewport.ZoomKeyOut := True + else if Key = gResKeys[kfZoomReset] then + fViewport.ResetZoom + else if (Key = gResKeys[kfMapDragScroll]) + and IsDragScrollingAllowed then + begin + fDragScrolling := True; + // Restrict the cursor to the window, for now. + //todo -cPractical: Allow one to drag out of the window, and still capture. + {$IFDEF MSWindows} + windowRect := gMain.ClientRect(1); //Reduce ClientRect by 1 pixel, to fix 'jump viewport' bug when dragscrolling over the window border + ClipCursor(@windowRect); + {$ENDIF} + fDragScrollingCursorPos.X := gCursor.Pixel.X; + fDragScrollingCursorPos.Y := gCursor.Pixel.Y; + fDragScrollingViewportPos.X := fViewport.Position.X; + fDragScrollingViewportPos.Y := fViewport.Position.Y; + gSystem.Cursor := kmcDrag; + end + else + aHandled := False; +end; + + +// This event happens every ~33ms if the Key is Down and holded +procedure TKMUserInterfaceGame.KeyDown(Key: Word; Shift: TShiftState; aIsFirst: Boolean; var aHandled: Boolean); +begin + if Assigned(fOnUserAction) then + fOnUserAction(uatKeyDown); + + if aHandled then Exit; + + inherited; + + // Update game options in case we used sounds hotkeys + if aHandled then + begin + OptionsChanged; + Exit; + end; + + HandleScrollKeysDown(Key, aHandled); +end; + + +procedure TKMUserInterfaceGame.HandleScrollKeysUp(Key: Word; var aHandled: Boolean); +begin + aHandled := True; + + if gMySpectator.Hand.InCinematic then + begin + aHandled := False; + Exit; + end; + + //Scrolling + if Key = gResKeys[kfScrollLeft] then + fViewport.ScrollKeyLeft := False + else if Key = gResKeys[kfScrollRight] then + fViewport.ScrollKeyRight := False + else if Key = gResKeys[kfScrollUp] then + fViewport.ScrollKeyUp := False + else if Key = gResKeys[kfScrollDown] then + fViewport.ScrollKeyDown := False + else if Key = gResKeys[kfZoomIn] then + fViewport.ZoomKeyIn := False + else if Key = gResKeys[kfZoomOut] then + fViewport.ZoomKeyOut := False + else if Key = gResKeys[kfZoomReset] then + fViewport.ResetZoom + else if Key = gResKeys[kfMapDragScroll] then + begin + if fDragScrolling then + ResetDragScrolling; + end + else + aHandled := False; +end; + + +procedure TKMUserInterfaceGame.KeyUp(Key: Word; Shift: TShiftState; var aHandled: Boolean); +begin + if Assigned(fOnUserAction) then + fOnUserAction(uatKeyUp); + + if aHandled then Exit; + + inherited; + + // Update game options in case we used sounds hotkeys + if aHandled then + begin + OptionsChanged; + Exit; + end; + + HandleScrollKeysUp(Key, aHandled); +end; + + +procedure TKMUserInterfaceGame.ResetDragScrolling; +begin + fDragScrolling := False; + gSystem.Cursor := kmcDefault; //Reset cursor + gMain.ApplyCursorRestriction; +end; + + +procedure TKMUserInterfaceGame.InitDebugControls; +begin + // Debugging displays + Bevel_DebugInfo := TKMBevel.Create(Panel_Main, ToolbarWidth + 8 - 10, 133 - 10, Panel_Main.Width - ToolbarWidth - 8, 0); + Bevel_DebugInfo.BackAlpha := 0.5; + Bevel_DebugInfo.Hitable := False; + Bevel_DebugInfo.Hide; + Label_DebugInfo := TKMLabel.Create(Panel_Main, ToolbarWidth + 8, 133, '', fntMonospaced, taLeft); + Label_DebugInfo.Hide; +end; + + +function TKMUserInterfaceGame.IsDragScrollingAllowed: Boolean; +begin + Result := True; // Allow drag scrolling by default +end; + + +procedure TKMUserInterfaceGame.MouseUp(Button: TMouseButton; Shift: TShiftState; X,Y: Integer); +begin + inherited; + if Assigned(fOnUserAction) then + fOnUserAction(uatMouseUp); +end; + + +procedure TKMUserInterfaceGame.MouseDown(Button: TMouseButton; Shift: TShiftState; X,Y: Integer); +begin + inherited; + + if Assigned(fOnUserAction) then + fOnUserAction(uatMouseDown); +end; + + +procedure TKMUserInterfaceGame.MouseMove(Shift: TShiftState; X,Y: Integer; var aHandled: Boolean); +var + VP: TKMPointF; +begin + inherited; + + if Assigned(fOnUserAction) then + fOnUserAction(uatMouseMove); + + aHandled := False; + if fDragScrolling then + begin + if GetKeyState(gResKeys[kfMapDragScroll]) < 0 then + begin + UpdateGameCursor(X, Y, Shift); + VP.X := fDragScrollingViewportPos.X + (fDragScrollingCursorPos.X - X) / (CELL_SIZE_PX * fViewport.Zoom); + VP.Y := fDragScrollingViewportPos.Y + (fDragScrollingCursorPos.Y - Y) / (CELL_SIZE_PX * fViewport.Zoom); + fViewport.Position := VP; + aHandled := True; + end else + ResetDragScrolling; + end; +end; + + +function TKMUserInterfaceGame.ZoomChangeBlocked: Boolean; +begin + Result := False; +end; + + +procedure TKMUserInterfaceGame.MouseWheel(Shift: TShiftState; WheelSteps, X, Y: Integer; var aHandled: Boolean); +var + prevCursor: TKMPointF; +begin + inherited; + + if Assigned(fOnUserAction) then + fOnUserAction(uatMouseWheel); + + if (X < 0) or (Y < 0) then Exit; // This happens when you use the mouse wheel on the window frame + + // Allow to zoom only when cursor is over map. Controls handle zoom on their own + if aHandled or ZoomChangeBlocked then Exit; + + UpdateGameCursor(X, Y, Shift); // Make sure we have the correct cursor position to begin with + prevCursor := gCursor.Float; + // +1 for ScrollSpeed = 0. + // Sqrt to reduce Scroll speed importance + // 11 = 10 + 1, 10 is default scroll speed + fViewport.Zoom := fViewport.Zoom * (1 + WheelSteps * Sqrt((gGameSettings.ScrollSpeed + 1) / 11) / 12); + UpdateGameCursor(X, Y, Shift); // Zooming changes the cursor position + // Move the center of the screen so the cursor stays on the same tile, thus pivoting the zoom around the cursor + fViewport.Position := KMPointF(fViewport.Position.X + prevCursor.X-gCursor.Float.X, + fViewport.Position.Y + prevCursor.Y-gCursor.Float.Y); + UpdateGameCursor(X, Y, Shift); // Recentering the map changes the cursor position + aHandled := True; +end; + + +procedure TKMUserInterfaceGame.PaintDefences; +var + I, K: Integer; + DP: TAIDefencePosition; + locF: TKMPointF; + screenLoc: TKMPoint; +begin + for I := 0 to gHands.Count - 1 do + for K := 0 to gHands[I].AI.General.DefencePositions.Count - 1 do + begin + DP := gHands[I].AI.General.DefencePositions[K]; + locF := gTerrain.FlatToHeight(KMPointF(DP.Position.Loc.X-0.5, DP.Position.Loc.Y-0.5)); + screenLoc := fViewport.MapToScreen(locF); + + if KMInRect(screenLoc, fViewport.ViewRect) then + begin + //Dir selector + TKMRenderUI.WritePicture(screenLoc.X, screenLoc.Y, 0, 0, [], rxGui, 510 + Byte(DP.Position.Dir)); + TKMRenderUI.WriteTextInShape(IntToStr(K+1), screenLoc.X, screenLoc.Y - 28, DEFENCE_LINE_TYPE_COL[DP.DefenceType], + FlagColorToTextColor(GROUP_TXT_COLOR[DP.GroupType]), $80000000, IntToStr(I + 1), gHands[I].FlagColor, icWhite); + //GroupType icon + TKMRenderUI.WritePicture(screenLoc.X, screenLoc.Y, 0, 0, [], rxGui, GROUP_IMG[DP.GroupType]); + end; + end; +end; + + +procedure TKMUserInterfaceGame.Paint; +begin + if (mlDefencesAll in gGameParams.VisibleLayers) then + fPaintDefences := True; + + if fPaintDefences then + PaintDefences; + + fPaintDefences := False; + + inherited; +end; + + +procedure TKMUserInterfaceGame.GameSpeedChanged(aFromSpeed, aToSpeed: Single); +begin + fViewport.GameSpeedChanged(aFromSpeed, aToSpeed); +end; + + +procedure TKMUserInterfaceGame.SyncUI(aMoveViewport: Boolean = True); +begin + fMinimap.LoadFromTerrain; + fMinimap.Update; + + gTerrain.OnTopHillChanged := fViewport.TopHillChanged; + + if aMoveViewport then + begin + fViewport.ResizeMap(gTerrain.MapX, gTerrain.MapY, gTerrain.TopHill / CELL_SIZE_PX); + fViewport.ResetZoom; + end; + + UpdateHotkeys; +end; + + +procedure TKMUserInterfaceGame.SyncUIView(const aCenter: TKMPointF); +begin + fViewport.Zoom := gGameSettings.DefaultZoom; // Set Zoom first, since it can apply restrictions on Position near map borders + fViewport.Position := aCenter; +end; + + +procedure TKMUserInterfaceGame.SyncUIView(const aCenter: TKMPointF; aZoom: Single); +begin + fViewport.Zoom := aZoom; // Set Zoom first, since it can apply restrictions on Position near map borders + fViewport.Position := aCenter; +end; + + +function TKMUserInterfaceGame.CursorToMapCoord(X, Y: Integer): TKMPointF; +begin + Result.X := fViewport.Position.X + (X-fViewport.ViewRect.Right/2-GetToolbarWidth/2)/CELL_SIZE_PX/fViewport.Zoom; + Result.Y := fViewport.Position.Y + (Y-fViewport.ViewRect.Bottom/2)/CELL_SIZE_PX/fViewport.Zoom; + Result.Y := gTerrain.ConvertCursorToMapCoord(Result.X, Result.Y); +end; + + +// Compute cursor position and store it in global variables +procedure TKMUserInterfaceGame.UpdateGameCursor(X, Y: Integer; Shift: TShiftState); +begin + UpdateCursor(X, Y, Shift); + + with gCursor do + begin + Float := CursorToMapCoord(X, Y); + + PrevCell := Cell; //Save previous cell + + // Cursor cannot reach row MapY or column MapX, they're not part of the map (only used for vertex height) + Cell.X := EnsureRange(Round(Float.X+0.5), 1, gTerrain.MapX-1); // Cell below cursor in map bounds + Cell.Y := EnsureRange(Round(Float.Y+0.5), 1, gTerrain.MapY-1); + + ObjectUID := gRenderPool.RenderList.GetSelectionUID(Float); + end; +end; + + +procedure TKMUserInterfaceGame.UpdateState(aGlobalTickCount: Cardinal); +begin + inherited; + + ShowDebugInfo; +end; + + +end. diff --git a/port-source/kam_remake/src/render/KM_Render.pas b/port-source/kam_remake/src/render/KM_Render.pas index fa09728..496e670 100644 --- a/port-source/kam_remake/src/render/KM_Render.pas +++ b/port-source/kam_remake/src/render/KM_Render.pas @@ -12,7 +12,12 @@ const TEX_FORMAT_SIZE: array [TKMTexFormat] of Byte = (2, 4, 1); type - TKMRenderMode = (rm2D, rm3D); + TKMRenderMode = (rm2D); + + TKMSampledVertex = record + X, Y, Z: Single; + U, V: Single; + end; //General OpenGL handling TKMRender = class @@ -34,10 +39,12 @@ type constructor Create(aScreenX, aScreenY: Integer); destructor Destroy; override; - procedure SetRenderMode(aRenderMode: TKMRenderMode); //Switch between 2D and 3D perspectives + procedure SetRenderMode(aRenderMode: TKMRenderMode); class procedure GraphicsContextChanged(aAvailable: Boolean); static; class function TextureBackendReady: Boolean; static; + class function SampledProgramReady: Boolean; static; + class procedure DrawSampledTriangles(const aVertices: array of TKMSampledVertex; const aColor: TKMColor4f); static; class function GetMaxTexSize: Cardinal; static; class function GetMaxViewportDim: Cardinal; static; class function GenerateTextureCommon(aMinFilter, aMagFilter: TKMFilterType): Cardinal; @@ -70,8 +77,9 @@ var implementation uses SysUtils, Math, + CastleRenderContext, CastleRenderOptions, KromOGLUtils, KromUtils, - KM_Log, KM_Defaults, KM_IoPNG, KM_CommonClasses, + KM_Defaults, KM_IoPNG, KM_CommonClasses, KM_Render_Mobile; const @@ -85,38 +93,23 @@ constructor TKMRender.Create(aScreenX, aScreenY: Integer); begin inherited Create; - gLog.AddTime('Init Render started'); - fFBOInited := False; if not fBlind then begin fMaxViewportDim := GetMaxViewportDim; - gLog.AddTime('GL_MAX_VIEWPORT_DIM = ' + IntToStr(fMaxViewportDim)); - glClearColor(0, 0, 0, 0); //Background - glClearStencil(0); - glDepthFunc(GL_LEQUAL); - glShadeModel(GL_SMOOTH); //Enables Smooth Color Shading - glPolygonMode(GL_FRONT, GL_FILL); - glEnable(GL_NORMALIZE); - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); //Set alpha mode - glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); - glEnable(GL_COLOR_MATERIAL); //Enable Materials - glEnable(GL_TEXTURE_2D); // Enable Texture Mapping - glDisable(GL_LIGHTING); //We don't need it //glEnable(GL_CULL_FACE); //glCullFace(GL_FRONT); end; Resize(aScreenX, aScreenY); - gLog.AddTime('Init Render Done'); end; destructor TKMRender.Destroy; begin + KMRenderMobileInvalidateSampledProgram; if not fBlind then begin if fFBOInited then @@ -171,19 +164,9 @@ end; procedure TKMRender.SetRenderMode(aRenderMode: TKMRenderMode); begin - if fBlind then Exit; - - glMatrixMode(GL_PROJECTION); //Change Matrix Mode to Projection - glLoadIdentity; //Reset View - //In 2D mode we use Z-test to clip terrain shadows behind mountains //1 unit for each tile strip. 512 means we can handle up to 512x512 maps - case aRenderMode of - rm2D: glOrtho(0, fScreenX, fScreenY, 0, -100, 100); - rm3D: gluPerspective(80, -fScreenX/fScreenY, 0.1, 5000); - end; - glMatrixMode(GL_MODELVIEW); //Return to the modelview matrix - glLoadIdentity; //Reset View + KMRenderMobileApplyOrdinary2DProjection(fScreenX, fScreenY); end; @@ -224,9 +207,15 @@ end; class procedure TKMRender.GraphicsContextChanged(aAvailable: Boolean); begin if aAvailable then - KMRenderMobileInitializeCurrentContext + begin + KMRenderMobileInitializeCurrentContext; + KMRenderMobileInitializeSampledProgram; + end else + begin KMRenderMobileInvalidateCurrentContext; + KMRenderMobileInvalidateSampledProgram; + end; end; @@ -236,6 +225,26 @@ begin end; +class function TKMRender.SampledProgramReady: Boolean; +begin + Result := KMRenderMobileSampledProgramReady; +end; + + +class procedure TKMRender.DrawSampledTriangles(const aVertices: array of TKMSampledVertex; const aColor: TKMColor4f); +begin + if Length(aVertices) = 0 then Exit; + if (Length(aVertices) mod 3) <> 0 then + raise Exception.Create('Sampled triangle vertex count must be divisible by three'); + + RenderContext.DepthFunc := dfLessEqual; + RenderContext.BlendingEnable(bsSrcAlpha, bdOneMinusSrcAlpha); + KMRenderMobileDrawSampledTriangles(@aVertices[0].X, @aVertices[0].U, + SizeOf(TKMSampledVertex), Length(aVertices), + aColor.R, aColor.G, aColor.B, aColor.A); +end; + + class function TKMRender.GetMaxTexSize: Cardinal; begin Result := KMRenderMobileMaxTextureSize; @@ -309,8 +318,6 @@ end; procedure TKMRender.BeginFrame; begin - if fBlind then Exit; - if SAVE_MAP_TO_FBO_RENDER then begin if not fFBOInited then @@ -320,31 +327,28 @@ begin glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fFBO); end; - glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT or GL_STENCIL_BUFFER_BIT); + KMRenderMobileClearCurrentTarget; SetRenderMode(rm2D); + KMRenderMobileSetActiveTextureUnit0; + TKMRender.InvalidateTextureBindingCache; + RenderContext.DepthFunc := dfLessEqual; + RenderContext.BlendingEnable(bsSrcAlpha, bdOneMinusSrcAlpha); + //RC.Activate for OSX end; //Render highlight overlay to make whole picture look brighter (more saturated) procedure TKMRender.RenderBrightness(aValue: Byte); +var + intensity: Single; begin - if fBlind then Exit; - //There will be no change to image anyway if aValue = 0 then Exit; - BindTexture(0); // We have to reset texture to default (0), because it could be bind to any other texture (atlas) - - glLoadIdentity; - glBlendFunc(GL_DST_COLOR, GL_ONE); - glColor4f(aValue/20, aValue/20, aValue/20, aValue/20); - glBegin(GL_QUADS); - glkRect(0, 0, ScreenX, ScreenY); - glEnd; - - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + intensity := aValue / 20; + KMRenderMobileDrawBrightnessOverlay(intensity, ScreenX, ScreenY); end; diff --git a/port-source/kam_remake/src/render/KM_RenderPool.pas b/port-source/kam_remake/src/render/KM_RenderPool.pas new file mode 100644 index 0000000..45914f6 --- /dev/null +++ b/port-source/kam_remake/src/render/KM_RenderPool.pas @@ -0,0 +1,2156 @@ +unit KM_RenderPool; +{$I KaM_Remake.inc} +interface +uses + {$IFDEF Unix} LCLIntf, LCLType, {$ENDIF} + KromOGLUtils, KromUtils, + KM_Defaults, KM_CommonTypes, KM_CommonClasses, KM_Pics, KM_Points, KM_Render, KM_Viewport, + KM_RenderTerrain, KM_Units, KM_HandEntity, + KM_Houses, KM_CommonGameTypes, KM_RenderDebug, + KM_ResTypes; + +type + TKMPaintLayer = (plTerrain, plObjects, plCursors); + + TKMRenderSprite = record + Loc: TKMPointF; // Where sprite lower-left corner is located + Feet: TKMPointF; // Feet of the sprite for FOW calculation (X;Y) and Z ordering (Y only) + RX: TRXType; + ID: Integer; + UID: Integer; + NewInst: Boolean; + TeamColor: Cardinal; + AlphaStep: Single; // Only apply-able to HouseBuild + SelectionRect: TKMRectF; // Used for selecting units by sprite + end; + + // List of sprites prepared to be rendered + TKMRenderList = class + private + fUnitsRXData: TRXData; //shortcut + fCount: Word; + fRenderOrder: array of Word; // Order in which sprites will be drawn () + fRenderList: array of TKMRenderSprite; + + procedure ClipRenderList; + procedure SendToRender(aId: Integer); + public + constructor Create; + destructor Destroy; override; + + procedure AddSprite(aRX: TRXType; aID: Integer; pX,pY: Single; aTeam: Cardinal = $0; aAlphaStep: Single = -1); + procedure AddSpriteG(aRX: TRXType; aID: Integer; aUID: Integer; pX,pY,gX,gY: Single; aTeam: Cardinal = $0; aAlphaStep: Single = -1); + + function GetSelectionUID(const aCurPos: TKMPointF): Integer; + procedure Clear; + procedure SortRenderList; + procedure Render; + end; + + // Collect everything that need to be rendered and put it in a list + TKMRenderPool = class + private + fCounter: Cardinal; + fRXData: array [TRXType] of TRXData; // Shortcuts + fViewport: TKMViewport; + // fSampleHouse: TOBJModel; + rPitch,rHeading,rBank: Integer; + fRenderList: TKMRenderList; + fRenderTerrain: TKMRenderTerrain; + fRenderDebug: TKMRenderDebug; + + fFieldsList: TKMPointTagList; + fHousePlansList: TKMPointDirList; + fTabletsList: TKMPointTagList; + fMarksList: TKMPointTagList; + fHouseOutline: TKMPointList; + + procedure ApplyTransform; + procedure SetDefaultRenderParams; + procedure RenderBackgroundUI(const aRect: TKMRect); + // Terrain overlay cursors rendering (incl. sprites highlighting) + procedure RenderForegroundUI; + procedure RenderForegroundUI_Brush; + procedure RenderForegroundUI_ElevateEqualize; + procedure RenderForegroundUI_ObjectsBrush; + procedure RenderForegroundUI_Markers; + procedure RenderForegroundUI_Units; + procedure RenderForegroundUI_PaintBucket(aHighlightAll: Boolean); + procedure RenderForegroundUI_UniversalEraser(aHighlightAll: Boolean); + procedure DoRenderGroup(aUnitType: TKMUnitType; aLoc: TKMPointDir; aMembersCnt, aUnitsPerRow: Integer; aHandColor: Cardinal); + function TryRenderUnitOrGroup(aEntity: TKMHandEntity; aUnitFilterFunc, aGroupFilterFunc: TBooleanFunc; aUseGroupFlagColor, aDoHighlight: Boolean; aHandColor, aFlagColor: Cardinal; aHighlightColor: Cardinal = 0): Boolean; + procedure RenderUnit(U: TKMUnit; const P: TKMPoint; aFlagColor: Cardinal; aDoHighlight: Boolean; aHighlightColor: Cardinal); overload; + procedure RenderUnit(aUnitType: TKMUnitType; const P: TKMPointDir; aAnimStep: Integer; aFlagColor: Cardinal; aDoHighlight: Boolean = False; aHighlightColor: Cardinal = 0); overload; + function PaintBucket_UnitToRender(aUnit: TObject): Boolean; + function PaintBucket_GroupToRender(aGroup: TObject): Boolean; + + procedure RenderSprite(aRX: TRXType; aId: Integer; aX, aY: Single; aColor: TColor4; aDoHighlight: Boolean = False; aHighlightColor: TColor4 = 0; + aForced: Boolean = False); + procedure RenderSpriteAlphaTest(aRX: TRXType; aId: Integer; aWoodProgress: Single; aX, aY: Single; aId2: Integer = 0; aStoneProgress: Single = 0; X2: Single = 0; Y2: Single = 0); + procedure RenderMapElement1(aIndex: Word; aAnimStep: Cardinal; aLocX, aLocY: Integer; aLoopAnim: Boolean; aDoImmediateRender: Boolean = False; aDeleting: Boolean = False); + procedure RenderMapElement4(aIndex: Word; aAnimStep: Cardinal; aLocX, aLocY: Integer; aIsDouble: Boolean; aDoImmediateRender: Boolean = False; aDeleting: Boolean = False); + procedure RenderHouseOutline(aHouseSketch: TKMHouseSketch; aCol: Cardinal = icCyan); + + // Terrain rendering sub-class + procedure CollectPlans(const aRect: TKMRect); + procedure CollectTerrainObjects(const aRect: TKMRect; aAnimStep: Cardinal); + procedure PaintFlagPoint(const aHouseEntrance, aFlagPoint: TKMPoint; aColor: Cardinal; aTexId: Integer; aFirstPass: Boolean; + aDoImmediateRender: Boolean = False); + procedure PaintFlagPoints(aFirstPass: Boolean); + + procedure RenderWireHousePlan(const P: TKMPoint; aHouseType: TKMHouseType); + procedure RenderTileOwnerLayer(const aRect: TKMRect); + procedure RenderTilesGrid(const aRect: TKMRect); + + procedure RenderWireTileInt(const X,Y: Integer); + procedure RenderTileInt(const X, Y: Integer); + public + constructor Create(aViewport: TKMViewport); + destructor Destroy; override; + + procedure ReInit; + + procedure AddAlert(const aLoc: TKMPointF; aId: Integer; aFlagColor: TColor4); + procedure AddProjectile(aProj: TKMProjectileType; const aRenderPos, aTilePos: TKMPointF; aDir: TKMDirection; aFlight: Single); + procedure AddHouse(aHouse: TKMHouseType; const aLoc: TKMPoint; aWoodStep, aStoneStep, aSnowStep: Single; aDoImmediateRender: Boolean = False; aDoHighlight: Boolean = False; aHighlightColor: TColor4 = 0); + procedure AddWholeHouse(H: TKMHouse; aFlagColor: Cardinal; aDoImmediateRender: Boolean = False; aDoHighlight: Boolean = False; aHighlightColor: TColor4 = 0); + + procedure AddHouseTablet(aHouse: TKMHouseType; const aLoc: TKMPoint); + procedure AddHouseBuildSupply(aHouse: TKMHouseType; const aLoc: TKMPoint; aWood, aStone: Byte); + procedure AddHouseWork(aHouse: TKMHouseType; const aLoc: TKMPoint; aActSet: TKMHouseActionSet; aAnimStep, aAnimStepPrev: Cardinal; aFlagColor: TColor4; aDoImmediateRender: Boolean = False; aDoHighlight: Boolean = False; aHighlightColor: TColor4 = 0); + procedure AddHouseSupply(aHouse: TKMHouseType; const aLoc: TKMPoint; const R1, R2: array of Word; const R3: array of Byte; aDoImmediateRender: Boolean = False; aDoHighlight: Boolean = False; aHighlightColor: TColor4 = 0); + procedure AddHouseMarketSupply(const aLoc: TKMPoint; aResType: TKMWareType; aResCount: Word; aAnimStep: Integer); + procedure AddHouseStableBeasts(aHouse: TKMHouseType; const aLoc: TKMPoint; aBeastId,aBeastAge,aAnimStep: Integer; aRX: TRXType = rxHouses); + procedure AddHouseEater(const aLoc: TKMPoint; aUnit: TKMUnitType; aAct: TKMUnitActionType; aDir: TKMDirection; aStepId: Integer; aOffX, aOffY: Single; aFlagColor: TColor4); + procedure AddUnit(aUnit: TKMUnitType; aUID: Integer; aAct: TKMUnitActionType; aDir: TKMDirection; StepId: Integer; StepFrac: Single; pX,pY: Single; FlagColor: TColor4; NewInst: Boolean; DoImmediateRender: Boolean = False; DoHighlight: Boolean = False; HighlightColor: TColor4 = 0); + procedure AddUnitCarry(aCarry: TKMWareType; aUID: Integer; aDir: TKMDirection; aStepId: Integer; aStepFrac: Single; pX,pY: Single; aFlagColor: TColor4); + procedure AddUnitThought(aUnit: TKMUnitType; aAct: TKMUnitActionType; aDir: TKMDirection; Thought: TKMUnitThought; pX,pY: Single); + procedure AddUnitFlag(aUnit: TKMUnitType; aAct: TKMUnitActionType; aDir: TKMDirection; FlagAnim: Integer; pX,pY: Single; FlagColor: TColor4; DoImmediateRender: Boolean = False); + procedure AddUnitWithDefaultArm(aUnit: TKMUnitType; aUID: Integer; aAct: TKMUnitActionType; aDir: TKMDirection; StepId: Integer; pX,pY: Single; FlagColor: TColor4; DoImmediateRender: Boolean = False; DoHignlight: Boolean = False; HighlightColor: TColor4 = 0); + + procedure RenderMapElement(aIndex: Word; aAnimStep,pX,pY: Integer; aDoImmediateRender: Boolean = False; aDeleting: Boolean = False); + procedure RenderSpriteOnTile(const aLoc: TKMPoint; aId: Integer; aFlagColor: TColor4 = $FFFFFFFF); + procedure RenderSpriteOnTerrain(const aLoc: TKMPointF; aId: Integer; aFlagColor: TColor4 = $FFFFFFFF; aForced: Boolean = False); + procedure RenderTile(aTerrainId: Word; pX,pY,Rot: Integer); + procedure RenderWireTile(const P: TKMPoint; aCol: TColor4; aInset: Single = 0.0; aLineWidth: Single = -1); + + property RenderDebug: TKMRenderDebug read fRenderDebug; + + property RenderList: TKMRenderList read fRenderList; + property RenderTerrain: TKMRenderTerrain read fRenderTerrain; + procedure SetRotation(aH,aP,aB: Integer); + + procedure Render(aTickLag: Single); + end; + + +var + gRenderPool: TKMRenderPool; + + +implementation +uses + Classes, SysUtils, Math, + dglOpenGL, + KM_Entity, + KM_RenderAux, KM_RenderGameAux, KM_HandsCollection, KM_Game, KM_GameSettings, + KM_AIFields, KM_TerrainPainter, KM_Cursor, + KM_Hand, KM_UnitGroup, KM_CommonUtils, + KM_GameParams, KM_Utils, KM_ResTilesetTypes, KM_DevPerfLog, KM_DevPerfLogTypes, + KM_HandTypes, + KM_Projectiles, + KM_Terrain, KM_TerrainTypes, + KM_Resource, KM_ResHouses, KM_ResInterpolation, KM_ResMapElements, KM_ResSprites, KM_ResUnits, + KM_AITypes; + + +const + DELETE_COLOR = $1616FF; + INTERP_LEVEL = 8; + + +{ TKMRenderPool } +constructor TKMRenderPool.Create(aViewport: TKMViewport); +var + RT: TRXType; +begin + inherited Create; + + fCounter := 0; + + for RT := Low(TRXType) to High(TRXType) do + fRXData[RT] := gRes.Sprites[RT].RXData; + + fViewport := aViewport; + + fRenderList := TKMRenderList.Create; + fRenderTerrain := TKMRenderTerrain.Create; + fRenderDebug := TKMRenderDebug.Create; + gRenderAux := TKMRenderAux.Create; + gRenderGameAux := TKMRenderGameAux.Create; + + fFieldsList := TKMPointTagList.Create; + fHousePlansList := TKMPointDirList.Create; + fTabletsList := TKMPointTagList.Create; + fMarksList := TKMPointTagList.Create; + fHouseOutline := TKMPointList.Create; + // fSampleHouse := TOBJModel.Create; + // fSampleHouse.LoadFromFile(ExeDir + 'Store.obj'); +end; + + +destructor TKMRenderPool.Destroy; +begin + fFieldsList.Free; + fHousePlansList.Free; + fTabletsList.Free; + fMarksList.Free; + fHouseOutline.Free; + // fSampleHouse.Free; + fRenderList.Free; + fRenderDebug.Free; + fRenderTerrain.Free; + gRenderGameAux.Free; + gRenderAux.Free; + + inherited; +end; + + +procedure TKMRenderPool.ReInit; +begin + if Self = nil then Exit; + + fRenderDebug.ReInit; +end; + + +procedure TKMRenderPool.SetRotation(aH, aP, aB: Integer); +begin + rHeading := aH; + rPitch := aP; + rBank := aB; +end; + + +procedure TKMRenderPool.ApplyTransform; +var + viewportPosRound: TKMPointF; +begin + //Need to round the viewport position so we only translate by whole pixels + viewportPosRound := RoundToTilePixel(fViewport.Position); + + glLoadIdentity; // Reset The View + + //Use integer division so we don't translate by half a pixel if clip is odd + glTranslatef(fViewport.ViewportClip.X div 2, fViewport.ViewportClip.Y div 2, 0); + + glScalef(fViewport.Zoom*CELL_SIZE_PX, fViewport.Zoom*CELL_SIZE_PX, 1 / 256); + + glTranslatef(-viewportPosRound.X + gGame.ActiveInterface.ToolbarWidth/CELL_SIZE_PX/fViewport.Zoom, -viewportPosRound.Y, 0); + + glRotatef(rHeading,1,0,0); + glRotatef(rPitch ,0,1,0); + glRotatef(rBank ,0,0,1); + glTranslatef(0, 0, -viewportPosRound.Y); +end; + + +procedure TKMRenderPool.SetDefaultRenderParams; +begin + glLineWidth(fViewport.Zoom * 2); + glPointSize(fViewport.Zoom * 5); + glEnable(GL_LINE_SMOOTH); +end; + + +// Render: +// 1. Sets viewport +// 2. Renders terrain +// 3. Polls Game objects to add themselves to RenderList through Add** methods +// 4. Renders cursor highlights +procedure TKMRenderPool.Render(aTickLag: Single); +var + clipRect: TKMRect; +begin + Inc(fCounter); + + ApplyTransform; + + glPushAttrib(GL_LINE_BIT or GL_POINT_BIT); + SetDefaultRenderParams; + + // Render only within visible area + clipRect := fViewport.GetClip; + + fRenderDebug.ClipRect := clipRect; + + // Collect players plans for terrain layer + CollectPlans(clipRect); + + // With depth test we can render all terrain tiles and then apply light/shadow without worrying about + // foothills shadows going over mountain tops. Each tile strip is rendered an next Z plane. + // Means that Z-test on gpu will take care of clipping the foothill shadows + glEnable(GL_DEPTH_TEST); + + // Everything flat of terrain + {$IFDEF DBG_PERFLOG} + gPerfLogs.SectionEnter(psFrameTerrain); + {$ENDIF} + fRenderTerrain.ClipRect := clipRect; + fRenderTerrain.RenderBase(gTerrain.AnimStep, gMySpectator.FogOfWar); + + // Disable depth test //and write to depth buffer, + // so that terrain shadows could be applied seamlessly ontop + glDisable(GL_DEPTH_TEST); + + if mlOverlays in gGameParams.VisibleLayers then + begin + fRenderTerrain.RenderFences(gMySpectator.FogOfWar); + fRenderTerrain.RenderPlayerPlans(fFieldsList, fHousePlansList); + end; + + if mlMiningRadius in gGameParams.VisibleLayers then + fRenderDebug.PaintMiningRadius; + + {$IFDEF DBG_PERFLOG} + gPerfLogs.SectionLeave(psFrameTerrain); + {$ENDIF} + + // House highlight, debug display + RenderBackgroundUI(clipRect); + + // Sprites are added by Terrain/Players/Projectiles, then sorted by position + fRenderList.Clear; + CollectTerrainObjects(clipRect, gTerrain.AnimStep); + + PaintFlagPoints(True); + + gHands.Paint(clipRect, aTickLag); // Units and houses + gProjectiles.Paint(aTickLag); + + if gGame.GamePlayInterface <> nil then + gGame.GamePlayInterface.Alerts.Paint(0); + + fRenderList.SortRenderList; + fRenderList.Render; + + if mlDefencesAll in gGameParams.VisibleLayers then + fRenderDebug.PaintDefences; + + fRenderTerrain.RenderFOW(gMySpectator.FogOfWar); + + // Alerts/rally second pass is rendered after FOW + PaintFlagPoints(False); + if gGame.GamePlayInterface <> nil then + gGame.GamePlayInterface.Alerts.Paint(1); + + // Cursor overlays (including blue-wire plans), go on top of everything + RenderForegroundUI; + + glPopAttrib; +end; + + +procedure TKMRenderPool.RenderBackgroundUI(const aRect: TKMRect); + + procedure HighlightUnit(U: TKMUnit; aCol: Cardinal); inline; + begin + gRenderAux.CircleOnTerrain(U.PositionF.X - 0.5 + U.GetSlide(axX), + U.PositionF.Y - 0.5 + U.GetSlide(axY), + 0.4, aCol, icCyan); + end; + + procedure HighlightEntity(aEntityH: TKMHighlightEntity); + var + I: Integer; + G: TKMUnitGroup; + col: Cardinal; + begin + if aEntityH.Entity = nil then Exit; + + case aEntityH.Entity.EntityType of + etHouse: RenderHouseOutline(TKMHouseSketch(aEntityH.Entity), aEntityH.Color); //fPositionF.X - 0.5 + GetSlide(axX), fPositionF.Y - 0.5 + GetSlide(axY), 0.35 + etUnit: HighlightUnit(TKMUnit(aEntityH.Entity), GetRandomColorWSeed(aEntityH.Entity.UID)); + etGroup: begin + G := TKMUnitGroup(aEntityH.Entity); + col := GetRandomColorWSeed(G.UID); + for I := 0 to G.Count - 1 do + HighlightUnit(G.Members[I], col); + end; + end; + end; + +begin + //Reset Texture, just in case we forgot to do it inside some method + TKMRender.BindTexture(0); // We have to reset texture to default (0), because it could be bind to any other texture (atlas) + + HighlightEntity(gMySpectator.HighlightEntity); + + if gGameParams.IsMapEditor then + gGame.MapEditor.Paint(plTerrain, aRect); + + if gAIFields <> nil then + gAIFields.Paint(aRect); + + if SHOW_TERRAIN_PASS <> 0 then + gRenderGameAux.Passability(aRect, SHOW_TERRAIN_PASS); + + if SHOW_TILES_OWNER then + RenderTileOwnerLayer(aRect); + + if SHOW_TERRAIN_TILES_GRID then + RenderTilesGrid(aRect); + +end; + + +procedure TKMRenderPool.CollectTerrainObjects(const aRect: TKMRect; aAnimStep: Cardinal); +var + I, K: Integer; +begin + if not (mlObjects in gGameParams.VisibleLayers) then Exit; + + if gGameParams.IsMapEditor then + gGame.MapEditor.Paint(plObjects, aRect); + + with gTerrain do + for I := aRect.Top to aRect.Bottom do + for K := aRect.Left to aRect.Right do + begin + if (Land^[I, K].Obj <> OBJ_NONE) then + RenderMapElement(Land^[I, K].Obj, AnimStep, K, I); + end; + + // Falling trees are in a separate list + with gTerrain do + for I := 0 to FallingTrees.Count - 1 do + begin + RenderMapElement1(FallingTrees.Tag[I], aAnimStep - FallingTrees.Tag2[I], FallingTrees[I].X, FallingTrees[I].Y, False); + Assert(AnimStep - FallingTrees.Tag2[I] <= 100, 'Falling tree overrun?'); + end; + + // Tablets on house plans, for self and allies + fTabletsList.Clear; + if gGameParams.IsReplayOrSpectate then + if gMySpectator.FOWIndex = -1 then + for I := 0 to gHands.Count - 1 do + gHands[I].GetPlansTablets(fTabletsList, aRect) + else + gHands[gMySpectator.FOWIndex].GetPlansTablets(fTabletsList, aRect) + else + gMySpectator.Hand.GetPlansTablets(fTabletsList, aRect); + + for I := 0 to fTabletsList.Count - 1 do + AddHouseTablet(TKMHouseType(fTabletsList.Tag[I]), fTabletsList[I]); +end; + + +procedure TKMRenderPool.PaintFlagPoint(const aHouseEntrance, aFlagPoint: TKMPoint; aColor: Cardinal; aTexId: Integer; aFirstPass: Boolean; + aDoImmediateRender: Boolean = False); + + procedure RenderLineToPoint(const aP: TKMPointF); + begin + gRenderAux.LineOnTerrain(aHouseEntrance.X - 0.5, aHouseEntrance.Y - 0.5, aP.X, aP.Y, aColor, $F0F0, False); + end; + +var + P: TKMPointF; +begin + P := KMPointF(aFlagPoint.X - 0.5, aFlagPoint.Y - 0.5); + if not aDoImmediateRender then + begin + if aFirstPass then + begin + AddAlert(P, aTexId, aColor); + RenderLineToPoint(P); + end + else + if gMySpectator.FogOfWar.CheckRevelation(P) < FOG_OF_WAR_MAX then + RenderSpriteOnTerrain(P, aTexId, aColor, True); //Force to paint, even under FOW + end + else begin + RenderSpriteOnTile(aFlagPoint, aTexId, aColor); + RenderLineToPoint(P); + end; +end; + + +procedure TKMRenderPool.PaintFlagPoints(aFirstPass: Boolean); +var + house: TKMHouseWFlagPoint; +begin + // Skip render if no house with flagpoint is chosen + if not (gMySpectator.Selected is TKMHouseWFlagPoint) then + Exit; + + house := TKMHouseWFlagPoint(gMySpectator.Selected); + if house.IsFlagPointSet then + PaintFlagPoint(house.Entrance, house.FlagPoint, gHands[house.Owner].GameFlagColor, gRes.Houses[house.HouseType].FlagPointTexId, aFirstPass); +end; + + +procedure TKMRenderPool.RenderTile(aTerrainId: Word; pX, pY, Rot: Integer); +begin + fRenderTerrain.RenderTile(aTerrainId, pX, pY, Rot); +end; + + +procedure TKMRenderPool.RenderMapElement(aIndex: Word; aAnimStep,pX,pY: Integer; aDoImmediateRender: Boolean = False; aDeleting: Boolean = False); +begin + if (gMySpectator.FogOfWar.CheckVerticeRenderRev(pX,pY) <= FOG_OF_WAR_MIN) then Exit;// Do not render tiles fully covered by FOW + // Render either normal object or quad depending on what it is + if gMapElements[aIndex].WineOrCorn then + RenderMapElement4(aIndex,aAnimStep,pX,pY,(aIndex in [54..57]),aDoImmediateRender,aDeleting) // 54..57 are grapes, all others are doubles + else + RenderMapElement1(aIndex,aAnimStep,pX,pY,True,aDoImmediateRender,aDeleting); +end; + + +procedure TKMRenderPool.RenderMapElement1(aIndex: Word; aAnimStep: Cardinal; aLocX, aLocY: Integer; aLoopAnim: Boolean; aDoImmediateRender: Boolean = False; + aDeleting: Boolean = False); +var + pX, pY: Integer; + cornerX, cornerY: Single; + gX, gY: Single; + Id, Id0: Integer; + FOW: Byte; +begin + if (gMySpectator.FogOfWar.CheckVerticeRenderRev(aLocX, aLocY) <= FOG_OF_WAR_MIN) then Exit; + + if aIndex = OBJ_BLOCK then + begin + // Invisible wall + // Render as a red outline in map editor mode + if gGameParams.IsMapEditor then + begin + gRenderAux.Quad(aLocX, aLocY, $600000FF); + RenderWireTile(KMPoint(aLocX, aLocY), $800000FF); + end; + end + else + begin + if gMapElements[aIndex].Anim.Count = 0 then Exit; + + if gGameParams.DynamicFOW then + begin + FOW := gMySpectator.FogOfWar.CheckTileRevelation(aLocX, aLocY); + if FOW <= 128 then aAnimStep := 0; // Stop animation + end; + Id := gRes.Interpolation.Tree(aIndex, aAnimStep, gGameParams.TickFrac, aLoopAnim); + Id0 := gMapElements[aIndex].Anim.Step[1] + 1; + if Id <= 0 then exit; + + var rxData: PRXData := @fRXData[rxTrees]; + pX := aLocX - 1; + pY := aLocY - 1; + gX := pX + (rxData.Pivot[Id0].X + rxData.Size[Id0].X/2) / CELL_SIZE_PX; + gY := pY + (rxData.Pivot[Id0].Y + rxData.Size[Id0].Y) / CELL_SIZE_PX; + cornerX := pX + rxData.Pivot[Id].X / CELL_SIZE_PX; + cornerY := pY - gTerrain.RenderHeightAt(gX, gY) + (rxData.Pivot[Id].Y + rxData.Size[Id].Y) / CELL_SIZE_PX; + if aDoImmediateRender then + RenderSprite(rxTrees, Id, cornerX, cornerY, $FFFFFFFF, aDeleting, DELETE_COLOR) + else + fRenderList.AddSpriteG(rxTrees, Id, 0, cornerX, cornerY, gX, gY); + end; +end; + + +// 4 objects packed on 1 tile for Corn and Grapes +procedure TKMRenderPool.RenderMapElement4(aIndex: Word; aAnimStep: Cardinal; aLocX, aLocY: Integer; aIsDouble: Boolean; aDoImmediateRender: Boolean = False; + aDeleting: Boolean = False); +var + R: TRXData; + + procedure AddSpriteBy(aAnimStep: Integer; aLocSubX, aLocSubY: Single); + var + Id, Id0: Integer; + CornerX, CornerY, gX, gY: Single; + begin + Id := gRes.Interpolation.Tree(aIndex, aAnimStep, gGameParams.TickFrac, True); + Id0 := gMapElements[aIndex].Anim.Step[1] + 1; + + gX := aLocSubX + (R.Pivot[Id0].X + R.Size[Id0].X/2) / CELL_SIZE_PX; + gY := aLocSubY + (R.Pivot[Id0].Y + R.Size[Id0].Y) / CELL_SIZE_PX; + CornerX := aLocSubX + R.Pivot[Id].X / CELL_SIZE_PX; + CornerY := aLocSubY - gTerrain.RenderHeightAt(gX, gY) + (R.Pivot[Id].Y + R.Size[Id].Y) / CELL_SIZE_PX; + + if aDoImmediateRender then + RenderSprite(rxTrees, Id, CornerX, CornerY, $FFFFFFFF, aDeleting, DELETE_COLOR) + else + fRenderList.AddSpriteG(rxTrees, Id, 0, CornerX, CornerY, gX, gY); + end; + +var + FOW: Byte; +begin + if gGameParams.DynamicFOW then + begin + FOW := gMySpectator.FogOfWar.CheckTileRevelation(aLocX, aLocY); + // Stop animation under FOW + if FOW <= 128 then + aAnimStep := 0; + end; + + R := fRXData[rxTrees]; + if aIsDouble then + begin + AddSpriteBy(aAnimStep , aLocX - 0.75, aLocY - 0.6); + AddSpriteBy(aAnimStep+1, aLocX - 0.25, aLocY - 0.6); + end + else + begin + AddSpriteBy(aAnimStep , aLocX - 0.75, aLocY - 0.75); + AddSpriteBy(aAnimStep+1, aLocX - 0.25, aLocY - 0.75); + AddSpriteBy(aAnimStep+1, aLocX - 0.75, aLocY - 0.25); + AddSpriteBy(aAnimStep , aLocX - 0.25, aLocY - 0.25); + end; +end; + + +// Render alert +procedure TKMRenderPool.AddAlert(const aLoc: TKMPointF; aId: Integer; aFlagColor: TColor4); +var + cornerX, cornerY: Single; + R: TRXData; +begin + R := fRXData[rxGui]; + + cornerX := aLoc.X + R.Pivot[aId].X / CELL_SIZE_PX; + cornerY := gTerrain.RenderFlatToHeight(aLoc).Y + R.Pivot[aId].Y / CELL_SIZE_PX; + + fRenderList.AddSpriteG(rxGui, aId, 0, cornerX, cornerY, aLoc.X, aLoc.Y, aFlagColor); +end; + + +// Render house WIP tablet +procedure TKMRenderPool.AddHouseTablet(aHouse: TKMHouseType; const aLoc: TKMPoint); +var + Id: Integer; + cornerX, cornerY, gX, gY: Single; + R: TRXData; +begin + R := fRXData[rxGui]; + Id := gRes.Houses[aHouse].TabletIcon; + + gX := aLoc.X + (R.Pivot[Id].X + R.Size[Id].X / 2) / CELL_SIZE_PX - 0.5; + gY := aLoc.Y + (R.Pivot[Id].Y + R.Size[Id].Y) / CELL_SIZE_PX - 0.45; + cornerX := aLoc.X + R.Pivot[Id].X / CELL_SIZE_PX - 0.25; + cornerY := aLoc.Y - gTerrain.RenderHeightAt(gX, gY) + (R.Pivot[Id].Y + R.Size[Id].Y) / CELL_SIZE_PX - 0.55; + fRenderList.AddSpriteG(rxGui, Id, 0, cornerX, cornerY, gX, gY); +end; + + +// Render house build supply +procedure TKMRenderPool.AddHouseBuildSupply(aHouse: TKMHouseType; const aLoc: TKMPoint; aWood, aStone: Byte); +var + rx: TRXData; + id: Integer; + houseBuildSupply: TKMHouseBuildSupply; + cornerX, cornerY: Single; +begin + rx := fRXData[rxHouses]; + houseBuildSupply := gRes.Houses[aHouse].BuildSupply; + + if aWood <> 0 then + begin + id := 260 + aWood - 1; + cornerX := aLoc.X + houseBuildSupply[1, aWood].MoveX / CELL_SIZE_PX - 1; + cornerY := aLoc.Y + (houseBuildSupply[1, aWood].MoveY + rx.Size[id].Y) / CELL_SIZE_PX - 1 + - gTerrain.LandExt^[aLoc.Y + 1, aLoc.X].RenderHeight / CELL_HEIGHT_DIV; + fRenderList.AddSprite(rxHouses, id, cornerX, cornerY); + end; + + if aStone <> 0 then + begin + id := 267 + aStone - 1; + cornerX := aLoc.X + houseBuildSupply[2, aStone].MoveX / CELL_SIZE_PX - 1; + cornerY := aLoc.Y + (houseBuildSupply[2, aStone].MoveY + rx.Size[id].Y) / CELL_SIZE_PX - 1 + - gTerrain.LandExt^[aLoc.Y + 1, aLoc.X].RenderHeight / CELL_HEIGHT_DIV; + fRenderList.AddSprite(rxHouses, id, cornerX, cornerY); + end; +end; + + +procedure TKMRenderPool.AddWholeHouse(H: TKMHouse; aFlagColor: Cardinal; aDoImmediateRender: Boolean = False; + aDoHighlight: Boolean = False; aHighlightColor: TColor4 = 0); +begin + if H <> nil then + begin + AddHouse(H.HouseType, H.Position, 1, 1, 0, aDoImmediateRender, aDoHighlight, aHighlightColor); + AddHouseSupply(H.HouseType, H.Position, H.WareInArray, H.WareOutArray, H.WareOutPoolArray, aDoImmediateRender, aDoHighlight, aHighlightColor); + if H.CurrentAction <> nil then + gRenderPool.AddHouseWork(H.HouseType, H.Position, H.CurrentAction.SubAction, H.WorkAnimStep, H.WorkAnimStepPrev, aFlagColor, aDoImmediateRender, aDoHighlight, aHighlightColor); + end; +end; + + +// Render house in wood +procedure TKMRenderPool.AddHouse(aHouse: TKMHouseType; const aLoc: TKMPoint; aWoodStep, aStoneStep, aSnowStep: Single; + aDoImmediateRender: Boolean = False; aDoHighlight: Boolean = False; aHighlightColor: TColor4 = 0); +var + rxData: TRXData; + picWood, picStone, picSnow: Integer; + groundWood, groundStone, gX, gY: Single; + + function CornerX(aPic: Integer): Single; + begin + Result := aLoc.X + rxData.Pivot[aPic].X / CELL_SIZE_PX - 1; + end; + + function CornerY(aPic: Integer): Single; + begin + Result := aLoc.Y + (rxData.Pivot[aPic].Y + rxData.Size[aPic].Y) / CELL_SIZE_PX - 1 + - gTerrain.LandExt^[aLoc.Y + 1, aLoc.X].RenderHeight / CELL_HEIGHT_DIV; + end; + +begin + // We cannot skip when WoodStep = 0 because building supply is rendered as a child. + // Instead RenderSpriteAlphaTest will skip rendering when WoodStep = 0 + + rxData := fRXData[rxHouses]; + + picWood := gRes.Houses[aHouse].WoodPic + 1; + picStone := gRes.Houses[aHouse].StonePic + 1; + picSnow := gRes.Houses[aHouse].SnowPic + 1; + + groundWood := rxData.Pivot[picWood].Y + rxData.Size[picWood].Y; + groundStone := rxData.Pivot[picStone].Y + rxData.Size[picStone].Y; + + gX := aLoc.X + (rxData.Pivot[picWood].X + rxData.Size[picWood].X / 2) / CELL_SIZE_PX - 1; + gY := aLoc.Y + Max(groundWood, groundStone) / CELL_SIZE_PX - 1.5; + + // If it's fully built we can render without alpha + if (aWoodStep = 1) and (aStoneStep = 1) then + begin + // Snow only happens on fully built houses + if gGameSettings.GFX.AllowSnowHouses + and (aSnowStep > 0) + and (picSnow <> 0) then + begin + // If snow is 100% we only need to render snow sprite + if aSnowStep = 1 then + fRenderList.AddSpriteG(rxHouses, picSnow, 0, CornerX(picSnow), CornerY(picSnow), gX, gY, $0) + else + begin + // Render stone with snow blended on top using AlphaTest + fRenderList.AddSpriteG(rxHouses, picStone, 0, CornerX(picStone), CornerY(picStone), gX, gY, $0); + fRenderList.AddSpriteG(rxHouses, picSnow, 0, CornerX(picSnow), CornerY(picSnow), gX, gY, $0, aSnowStep); + end; + end + else if aDoImmediateRender then + RenderSprite(rxHouses, picStone, CornerX(picStone), CornerY(picStone), $0, aDoHighlight, aHighlightColor) + else + fRenderList.AddSpriteG(rxHouses, picStone, 0, CornerX(picStone), CornerY(picStone), gX, gY, $0); + end + else + begin + // Wood part of the house (may be seen below Stone part before construction is complete, e.g. Sawmill) + fRenderList.AddSpriteG(rxHouses, picWood, 0, CornerX(picWood), CornerY(picWood), gX, gY, $0, aWoodStep); + if aStoneStep > 0 then + fRenderList.AddSprite(rxHouses, picStone, CornerX(picStone), CornerY(picStone), $0, aStoneStep); + end; +end; + + +procedure TKMRenderPool.AddHouseWork(aHouse: TKMHouseType; const aLoc: TKMPoint; aActSet: TKMHouseActionSet; aAnimStep, aAnimStepPrev: Cardinal; + aFlagColor: TColor4; aDoImmediateRender: Boolean = False; aDoHighlight: Boolean = False; aHighlightColor: TColor4 = 0); +var + id: Cardinal; + AT: TKMHouseActionType; + A: TKMAnimLoop; + rxData: TRXData; + cornerX, cornerY: Single; +const + //These house actions should animate smoothly and continuously, regardless of AnimStep + CONTINUOUS_ANIMS: TKMHouseActionSet = [ + haSmoke, haFlagpole, + haFlag1, haFlag2, haFlag3, + haFire1, haFire2, haFire3, haFire4, haFire5, haFire6, haFire7, haFire8 + ]; +begin + if aActSet = [] then Exit; + + rxData := fRXData[rxHouses]; + + //See if action is in set and render it + for AT := Low(TKMHouseActionType) to High(TKMHouseActionType) do + if AT in aActSet then + begin + A := gRes.Houses[aHouse].Anim[AT]; + if A.Count > 0 then + begin + if AT in CONTINUOUS_ANIMS then + id := gRes.Interpolation.House(aHouse, AT, gTerrain.AnimStep, gGameParams.TickFrac) + else + begin + //If the anim step is able to be interpolated from the last frame (to avoid incorrect looping) + if aAnimStep = aAnimStepPrev+1 then + id := gRes.Interpolation.House(aHouse, AT, aAnimStepPrev, gGameParams.TickFrac) + else + id := A.Step[aAnimStep mod Byte(A.Count) + 1] + 1; + end; + + cornerX := aLoc.X + (rxData.Pivot[id].X + A.MoveX) / CELL_SIZE_PX - 1; + cornerY := aLoc.Y + (rxData.Pivot[id].Y + A.MoveY + rxData.Size[id].Y) / CELL_SIZE_PX - 1 + - gTerrain.LandExt^[aLoc.Y + 1, aLoc.X].RenderHeight / CELL_HEIGHT_DIV; + + if aDoImmediateRender then + RenderSprite(rxHouses, id, cornerX, cornerY, aFlagColor, aDoHighlight, aHighlightColor) + else + fRenderList.AddSprite(rxHouses, id, cornerX, cornerY, aFlagColor); + end; + end; +end; + + +procedure TKMRenderPool.AddHouseSupply(aHouse: TKMHouseType; const aLoc: TKMPoint; const R1, R2: array of Word; const R3: array of Byte; + aDoImmediateRender: Boolean = False; aDoHighlight: Boolean = False; aHighlightColor: TColor4 = 0); +var + id, I, K, I2, count: Integer; + rxData: TRXData; + + procedure AddHouseSupplySprite(aId: Integer); + var + CornerX, CornerY: Single; + begin + if aId = 0 then Exit; + + CornerX := aLoc.X + rxData.Pivot[aId].X / CELL_SIZE_PX - 1; + CornerY := aLoc.Y + (rxData.Pivot[aId].Y + rxData.Size[aId].Y) / CELL_SIZE_PX - 1 + - gTerrain.LandExt^[aLoc.Y + 1, aLoc.X].RenderHeight / CELL_HEIGHT_DIV; + if aDoImmediateRender then + begin + RenderSprite(rxHouses, aId, CornerX, CornerY, $0, aDoHighlight, aHighlightColor); + end else + fRenderList.AddSprite(rxHouses, aId, CornerX, CornerY); + end; + +begin + rxData := fRXData[rxHouses]; + + for I := 1 to 4 do + if R1[I - 1] > 0 then + begin + count := Min(R1[I - 1], MAX_WARES_IN_HOUSE); + I2 := I; + + // Need to swap Coal and Steel for the ArmorSmithy + // For some reason KaM stores these wares in swapped order, here we fix it (1 <-> 2) + if (aHouse = htArmorSmithy) and (I in [1,2]) then + I2 := 3-I; + + // Need to swap Timber and Leather for the ArmorWorkshop + // For some reason KaM stores these wares in swapped order, here we fix it (1 <-> 2) + if (aHouse = htArmorWorkshop) and (I in [1,2]) then + I2 := 3-I; + + id := gRes.Houses[aHouse].SupplyIn[I2, count] + 1; + AddHouseSupplySprite(id); + end; + + if gRes.Houses[aHouse].IsWorkshop then + begin + for K := 0 to 19 do + if R3[K] > 0 then + begin + I2 := R3[K]; + + // Need to swap Shields and Armor for the ArmorWorkshop + // For some reason KaM stores these wares in swapped order, here we fix it (1 <-> 2) +// if (aHouse = htArmorWorkshop) and (I2 in [1,2]) then +// I2 := 3-R3[K]; + + id := gRes.Houses[aHouse].SupplyOut[I2, K mod MAX_WARES_IN_HOUSE + 1] + 1; + AddHouseSupplySprite(id); + end; + end + else + begin + for I := 1 to 4 do + if R2[I - 1] > 0 then + begin + count := Min(R2[I - 1], MAX_WARES_IN_HOUSE); + id := gRes.Houses[aHouse].SupplyOut[I, count] + 1; + AddHouseSupplySprite(id); + end; + end; +end; + + +procedure TKMRenderPool.AddHouseMarketSupply(const aLoc: TKMPoint; aResType: TKMWareType; aResCount: Word; aAnimStep: Integer); +var + I, id: Integer; + cornerX, cornerY: Single; + rxData: TRXData; +begin + if aResType = wtHorse then // Horses are a beast, BeastId is the count, age is 1 + for I := 1 to Min(aResCount, MARKET_WARES[aResType].Count) do // Render each beast + AddHouseStableBeasts(htMarket, aLoc, I, 1, aAnimStep, rxHouses) + else + begin + if MARKET_WARES[aResType].Count = 0 then Exit; + id := (MARKET_WARES[aResType].TexStart-1) + Min(aResCount, MARKET_WARES[aResType].Count); + if id = 0 then Exit; + + rxData := fRXData[rxHouses]; + cornerX := aLoc.X + (rxData.Pivot[id].X + MARKET_WARES_OFF_X) / CELL_SIZE_PX - 1; + cornerY := aLoc.Y + (rxData.Pivot[id].Y + MARKET_WARES_OFF_Y + rxData.Size[id].Y) / CELL_SIZE_PX - 1 + - gTerrain.LandExt^[aLoc.Y+1,aLoc.X].RenderHeight / CELL_HEIGHT_DIV; + fRenderList.AddSprite(rxHouses, id, cornerX, cornerY); + end; +end; + + +procedure TKMRenderPool.AddHouseStableBeasts(aHouse: TKMHouseType; const aLoc: TKMPoint; aBeastId, aBeastAge, aAnimStep: Integer; aRX: TRXType = rxHouses); +var + cornerX, cornerY: Single; + id: Integer; + rxData: TRXData; + A: TKMAnimLoop; +begin + rxData := fRXData[aRX]; + + A := gRes.Houses.BeastAnim[aHouse,aBeastId,aBeastAge]; + + id := gRes.Interpolation.Beast(aHouse, aBeastId, aBeastAge, aAnimStep, gGameParams.TickFrac); + + cornerX := aLoc.X + (A.MoveX + rxData.Pivot[id].X) / CELL_SIZE_PX - 1; + cornerY := aLoc.Y + (A.MoveY + rxData.Pivot[id].Y + rxData.Size[id].Y) / CELL_SIZE_PX - 1 + - gTerrain.LandExt^[aLoc.Y + 1, aLoc.X].RenderHeight / CELL_HEIGHT_DIV; + fRenderList.AddSprite(aRX, id, cornerX, cornerY); +end; + + +// aRenderPos has gTerrain.HeightAt factored in already, aTilePos is on tile coordinates for Z ordering +procedure TKMRenderPool.AddProjectile(aProj: TKMProjectileType; const aRenderPos, aTilePos: TKMPointF; aDir: TKMDirection; aFlight: Single); +var + FOW: Byte; + id: Integer; + rxData: TRXData; + cornerX, cornerY: Single; + ground: Single; +begin + // We don't care about off-map arrows, but still we get TKMPoint error if X/Y gets negative + if not gTerrain.TileInMapCoords(Round(aRenderPos.X), Round(aRenderPos.Y)) then Exit; + + if gGameParams.DynamicFOW then + begin + FOW := gMySpectator.FogOfWar.CheckRevelation(aRenderPos); + if FOW <= 128 then Exit; // Don't render objects which are behind FOW + end; + + case aProj of + ptArrow: id := gRes.Interpolation.UnitActionByPercent(utBowman, uaSpec, aDir, aFlight); + ptBolt: id := gRes.Interpolation.UnitActionByPercent(utCrossbowman, uaSpec, aDir, aFlight); + ptSlingRock: id := gRes.Interpolation.UnitActionByPercent(utRogue, uaSpec, aDir, aFlight); + ptTowerRock: id := gRes.Interpolation.UnitActionByPercent(utRecruit, uaSpec, aDir, aFlight); + else id := 1; // Nothing? + end; + + rxData := fRXData[rxUnits]; + + cornerX := rxData.Pivot[id].X / CELL_SIZE_PX - 1; + cornerY := (rxData.Pivot[id].Y + rxData.Size[id].Y) / CELL_SIZE_PX - 1; + + case aProj of + ptArrow, ptBolt, ptSlingRock: ground := aTilePos.Y + (0.5 - Abs(Min(aFlight, 1) - 0.5)) - 0.5; + ptTowerRock: ground := aTilePos.Y + Min(aFlight, 1)/5 - 0.4; + else ground := aTilePos.Y - 1; // Nothing? + end; + + fRenderList.AddSpriteG(rxUnits, id, 0, aRenderPos.X + cornerX, aRenderPos.Y + cornerY, aTilePos.X - 1, ground); +end; + + +procedure TKMRenderPool.AddUnit(aUnit: TKMUnitType; aUID: Integer; aAct: TKMUnitActionType; aDir: TKMDirection; StepId: Integer; StepFrac: Single; + pX,pY: Single; FlagColor: TColor4; NewInst: Boolean; DoImmediateRender: Boolean = False; + DoHighlight: Boolean = False; HighlightColor: TColor4 = 0); +var + cornerX, cornerY, ground: Single; + id, id0: Integer; + R: TRXData; +begin + id := gRes.Interpolation.UnitAction(aUnit, aAct, aDir, StepId, StepFrac); + id0 := gRes.Interpolation.UnitAction(aUnit, aAct, aDir, UNIT_STILL_FRAMES[aDir], 0.0); + if id <= 0 then exit; + R := fRXData[rxUnits]; + + cornerX := pX + R.Pivot[id].X / CELL_SIZE_PX; + cornerY := gTerrain.RenderFlatToHeight(pX, pY) + (R.Pivot[id].Y + R.Size[id].Y) / CELL_SIZE_PX; + ground := pY + (R.Pivot[id0].Y + R.Size[id0].Y) / CELL_SIZE_PX; + + if DoImmediateRender then + RenderSprite(rxUnits, id, cornerX, cornerY, FlagColor, DoHighlight, HighlightColor) + else + if NewInst then + fRenderList.AddSpriteG(rxUnits, id, aUID, cornerX, cornerY, pX, ground, FlagColor) + else + fRenderList.AddSprite(rxUnits, id, cornerX, cornerY, FlagColor); + +end; + + +procedure TKMRenderPool.AddHouseEater(const aLoc: TKMPoint; aUnit: TKMUnitType; aAct: TKMUnitActionType; aDir: TKMDirection; aStepId: Integer; aOffX, aOffY: Single; aFlagColor: TColor4); +var + cornerX, cornerY: Single; + id: Integer; + R: TRXData; +begin + id := gRes.Interpolation.UnitAction(aUnit, aAct, aDir, aStepId, gGameParams.TickFrac); + if id <= 0 then exit; + R := fRXData[rxUnits]; + + // Eaters need to interpolate land height the same as the inn otherwise they are rendered at the wrong place + cornerX := aLoc.X + aOffX + R.Pivot[id].X / CELL_SIZE_PX - 1; + cornerY := aLoc.Y + aOffY + (R.Pivot[id].Y + R.Size[id].Y) / CELL_SIZE_PX - 1 + - gTerrain.LandExt^[aLoc.Y + 1, aLoc.X].RenderHeight / CELL_HEIGHT_DIV; + + fRenderList.AddSprite(rxUnits, id, cornerX, cornerY, aFlagColor); +end; + + +procedure TKMRenderPool.AddUnitCarry(aCarry: TKMWareType; aUID: Integer; aDir: TKMDirection; aStepId: Integer; aStepFrac: Single; pX,pY: Single; aFlagColor: TColor4); +var + cornerX, cornerY: Single; + id: Integer; + A: TKMAnimLoop; + R: TRXData; +begin + A := gRes.Units.SerfCarry[aCarry, aDir]; + id := gRes.Interpolation.SerfCarry(aCarry, aDir, aStepId, aStepFrac); + + if id <= 0 then Exit; + R := fRXData[rxUnits]; + + cornerX := pX + (R.Pivot[id].X + A.MoveX) / CELL_SIZE_PX; + cornerY := gTerrain.RenderFlatToHeight(pX, pY) + (R.Pivot[id].Y + R.Size[id].Y + A.MoveY) / CELL_SIZE_PX; + fRenderList.AddSprite(rxUnits, id, cornerX, cornerY, aFlagColor); +end; + + +procedure TKMRenderPool.AddUnitThought(aUnit: TKMUnitType; aAct: TKMUnitActionType; + aDir: TKMDirection; + Thought: TKMUnitThought; pX,pY: Single); +var + cornerX, cornerY, ground: Single; + R: TRXData; + A: TKMAnimLoop; + id, id0: Integer; +begin + if Thought = thNone then Exit; + R := fRXData[rxUnits]; + + // Unit position + A := gRes.Units[aUnit].UnitAnim[aAct, aDir]; + id0 := A.Step[UNIT_STILL_FRAMES[aDir] mod Byte(A.Count) + 1] + 1; + + // Units feet + ground := pY + (R.Pivot[id0].Y + R.Size[id0].Y) / CELL_SIZE_PX; + // The thought should be slightly lower than the unit so it goes OVER warrior flags + ground := ground + THOUGHT_X_OFFSET; + + id := gRes.Interpolation.UnitThought(Thought, gTerrain.AnimStep, gGameParams.TickFrac); + + cornerX := pX + R.Pivot[id].X / CELL_SIZE_PX; + cornerY := gTerrain.RenderFlatToHeight(pX, pY) + (R.Pivot[id].Y + R.Size[id].Y) / CELL_SIZE_PX - 1.5; + fRenderList.AddSpriteG(rxUnits, id, 0, cornerX, cornerY, pX, ground); +end; + + +procedure TKMRenderPool.AddUnitFlag(aUnit: TKMUnitType; aAct: TKMUnitActionType; aDir: TKMDirection; + FlagAnim: Integer; pX, pY: Single; FlagColor: TColor4; DoImmediateRender: Boolean = False); +const + // Offsets for flags rendering in pixels + FlagXOffset: array [GROUP_TYPE_MIN..GROUP_TYPE_MAX, TKMDirection] of shortint = ( + ( 0, 10, -1, 2, 1, -6,-10, 4, 13), // gtMelee + ( 0, 6, 5, 7, -3,-10, -4, 10, 9), // gtAntiHorse + ( 0, 8, 6, 6, -6, -8, -3, 8, 6), // gtRanged + ( 0, 6, 2, 3, -5,-10, -8, 5, 6)); // gtMounted + + FlagYOffset: array [GROUP_TYPE_MIN..GROUP_TYPE_MAX, TKMDirection] of shortint = ( + ( 0, 28, 30, 30, 26, 25, 24, 25, 27), // gtMelee + ( 0, 23, 25, 25, 21, 20, 19, 20, 22), // gtAntiHorse + ( 0, 28, 30, 30, 26, 25, 24, 25, 27), // gtRanged + ( 0, 4, 16, 16, 4, 5, 2, 3, 4)); // gtMounted +var + R: TRXData; + A: TKMAnimLoop; + id0, idFlag: Integer; + flagX, flagY, ground: Single; +begin + R := fRXData[rxUnits]; + + // Unit position + A := gRes.Units[aUnit].UnitAnim[aAct, aDir]; + id0 := A.Step[UNIT_STILL_FRAMES[aDir] mod Byte(A.Count) + 1] + 1; + + // Units feet + ground := pY + (R.Pivot[id0].Y + R.Size[id0].Y) / CELL_SIZE_PX; + + // Flag position + idFlag := gRes.Interpolation.UnitAction(aUnit, uaWalkArm, aDir, FlagAnim, gGameParams.TickFrac); + if idFlag <= 0 then Exit; + + flagX := pX + (R.Pivot[idFlag].X + FlagXOffset[UNIT_TO_GROUP_TYPE[aUnit], aDir]) / CELL_SIZE_PX - 0.5; + flagY := gTerrain.RenderFlatToHeight(pX, pY) + (R.Pivot[idFlag].Y + FlagYOffset[UNIT_TO_GROUP_TYPE[aUnit], aDir] + R.Size[idFlag].Y) / CELL_SIZE_PX - 2.25; + + if DoImmediateRender then + RenderSprite(rxUnits, idFlag, flagX, flagY, FlagColor) + else + fRenderList.AddSpriteG(rxUnits, idFlag, 0, flagX, flagY, pX, ground, FlagColor); +end; + + +procedure TKMRenderPool.AddUnitWithDefaultArm(aUnit: TKMUnitType; aUID: Integer; aAct: TKMUnitActionType; aDir: TKMDirection; StepId: Integer; pX,pY: Single; FlagColor: TColor4; DoImmediateRender: Boolean = False; DoHignlight: Boolean = False; HighlightColor: TColor4 = 0); +begin + if aUnit = utFish then + aAct := TKMUnitFish.GetFishActionType(UNIT_FISH_CONUT_DEFAULT); // In map editor always render default fish + + AddUnit(aUnit, aUID, aAct, aDir, StepId, 0.0, pX, pY, FlagColor, True, DoImmediateRender, DoHignlight, HighlightColor); + if gRes.Units[aUnit].SupportsAction(uaWalkArm) then + AddUnit(aUnit, aUID, uaWalkArm, aDir, StepId, 0.0, pX, pY, FlagColor, True, DoImmediateRender, DoHignlight, HighlightColor); +end; + + +{procedure TRenderPool.RenderObject(aRX: TRXType; aId: Word; pX,pY: Single); +type + TVector4f = record X,Y,Z,W: Single; end; + TColor4f = record R,G,B,A: Single; end; +const + LightPos: TVector4f = (X:-1; Y:0; Z:-2; W:0); + LightAmb: TColor4f = (R:0.1; G:0.1; B:0.1; A:0); + LightDiff: TColor4f = (R:0.9; G:0.9; B:0.9; A:0); + LightSpec: TColor4f = (R:1.0; G:1.0; B:1.0; A:0); +begin + glPushMatrix; + glPushAttrib(GL_LIGHTING_BIT or GL_DEPTH_BUFFER_BIT); + glScalef(1, 1, CELL_SIZE_PX); + glEnable(GL_DEPTH_TEST); + glEnable(GL_LIGHTING); + glEnable(GL_LIGHT0); + glLightfv(GL_LIGHT0, GL_AMBIENT, @LightAmb); + glLightfv(GL_LIGHT0, GL_DIFFUSE, @LightDiff); + glLightfv(GL_LIGHT0, GL_SPECULAR, @LightSpec); + glLightfv(GL_LIGHT0, GL_POSITION, @LightPos); + + glLightModelf(GL_LIGHT_MODEL_LOCAL_VIEWER, 0.0); //Directional lighting + glLightModeli(GL_LIGHT_MODEL_COLOR_CONTROL, GL_SEPARATE_SPECULAR_COLOR); //Specular does not depend on material color + + glTranslatef(pX, pY, -1); + glRotatef(32.5, 1, 0, 0); + glColor4f(0.8, 0.8, 0.8, 1); + + fSampleHouse.DrawModel; + glPopAttrib; + glPopMatrix; +end;} + + +procedure TKMRenderPool.RenderSprite(aRX: TRXType; aId: Integer; aX, aY: Single; aColor: TColor4; aDoHighlight: Boolean = False; + aHighlightColor: TColor4 = 0; aForced: Boolean = False); +var + tX, tY: Integer; + rX, rY: Single; +begin + tX := EnsureRange(Round(aX), 1, gTerrain.MapX); + tY := EnsureRange(Round(aY), 1, gTerrain.MapY); + //Do not render if sprite is under FOW + if not aForced and (gMySpectator.FogOfWar.CheckVerticeRenderRev(tX, tY) <= FOG_OF_WAR_MIN) then + Exit; + + rX := RoundToTilePixel(aX); + rY := RoundToTilePixel(aY); + + with gGFXData[aRX, aId] do + begin + // FOW is rendered over the top so no need to make sprites black anymore + glColor4ub(255, 255, 255, 255); + + TKMRender.BindTexture(Tex.TexID); + if aDoHighlight then + glColor3ub(aHighlightColor and $FF, aHighlightColor shr 8 and $FF, aHighlightColor shr 16 and $FF); + glBegin(GL_QUADS); + glTexCoord2f(Tex.u1, Tex.v2); glVertex2f(rX , rY ); + glTexCoord2f(Tex.u2, Tex.v2); glVertex2f(rX+pxWidth/CELL_SIZE_PX, rY ); + glTexCoord2f(Tex.u2, Tex.v1); glVertex2f(rX+pxWidth/CELL_SIZE_PX, rY-pxHeight/CELL_SIZE_PX); + glTexCoord2f(Tex.u1, Tex.v1); glVertex2f(rX , rY-pxHeight/CELL_SIZE_PX); + glEnd; + end; + + if gGFXData[aRX, aId].Alt.TexID <> 0 then + with gGFXData[aRX, aId] do + begin + glColor4ubv(@aColor); + TKMRender.BindTexture(Alt.TexID); + glBegin(GL_QUADS); + glTexCoord2f(Alt.u1, Alt.v2); glVertex2f(rX , rY ); + glTexCoord2f(Alt.u2, Alt.v2); glVertex2f(rX+pxWidth/CELL_SIZE_PX, rY ); + glTexCoord2f(Alt.u2, Alt.v1); glVertex2f(rX+pxWidth/CELL_SIZE_PX, rY-pxHeight/CELL_SIZE_PX); + glTexCoord2f(Alt.u1, Alt.v1); glVertex2f(rX , rY-pxHeight/CELL_SIZE_PX); + glEnd; + end; +end; + + +// Param - defines at which level alpha-test will be set (acts like a threshhold) +// Then we render alpha-tested Mask to stencil buffer. Only those pixels that are +// white there will have sprite rendered +// If there are two masks then we need to render sprite only there +// where its mask is white AND where second mask is black +procedure TKMRenderPool.RenderSpriteAlphaTest(aRX: TRXType; aId: Integer; aWoodProgress: Single; aX, aY: Single; + aId2: Integer = 0; aStoneProgress: Single = 0; X2: Single = 0; Y2: Single = 0); +var + tX, tY: Integer; + rX, rY: Single; +begin + // Skip rendering if alphas are zero (occurs so non-started houses can still have child sprites) + if (aWoodProgress = 0) and (aStoneProgress = 0) then Exit; + + tX := EnsureRange(Round(aX), 1, gTerrain.MapX); + tY := EnsureRange(Round(aY), 1, gTerrain.MapY); + if gMySpectator.FogOfWar.CheckVerticeRenderRev(tX, tY) <= FOG_OF_WAR_MIN then Exit; + + rX := RoundToTilePixel(aX); + rY := RoundToTilePixel(aY); + + X2 := RoundToTilePixel(X2); + Y2 := RoundToTilePixel(Y2); + + glClear(GL_STENCIL_BUFFER_BIT); + + // Setup stencil mask + glEnable(GL_STENCIL_TEST); + glStencilFunc(GL_ALWAYS, 1, 1); + glStencilOp(GL_REPLACE, GL_REPLACE, GL_REPLACE); + + glPushAttrib(GL_COLOR_BUFFER_BIT); + // Do not render anything on screen while setting up stencil mask + glColorMask(False, False, False, False); + + // Prepare stencil mask. Sprite will be rendered only where are white pixels + glEnable(GL_ALPHA_TEST); + glBlendFunc(GL_ONE, GL_ZERO); + + // Wood progress + glAlphaFunc(GL_GREATER, 1 - aWoodProgress); + with gGFXData[aRX,aId] do + begin + glColor3f(1, 1, 1); + TKMRender.BindTexture(Alt.TexID); + glBegin(GL_QUADS); + glTexCoord2f(Alt.u1,Alt.v2); glVertex2f(rX , rY ); + glTexCoord2f(Alt.u2,Alt.v2); glVertex2f(rX+pxWidth/CELL_SIZE_PX, rY ); + glTexCoord2f(Alt.u2,Alt.v1); glVertex2f(rX+pxWidth/CELL_SIZE_PX, rY-pxHeight/CELL_SIZE_PX); + glTexCoord2f(Alt.u1,Alt.v1); glVertex2f(rX , rY-pxHeight/CELL_SIZE_PX); + glEnd; + TKMRender.BindTexture(0); + end; + + // Stone progress + if aId2 <> 0 then + begin + glStencilOp(GL_DECR, GL_DECR, GL_DECR); + + glAlphaFunc(GL_GREATER, 1 - aStoneProgress); + with gGFXData[aRX,aId2] do + begin + glColor3f(1, 1, 1); + TKMRender.BindTexture(Alt.TexID); + glBegin(GL_QUADS); + glTexCoord2f(Alt.u1,Alt.v2); glVertex2f(X2 ,Y2 ); + glTexCoord2f(Alt.u2,Alt.v2); glVertex2f(X2+pxWidth/CELL_SIZE_PX,Y2 ); + glTexCoord2f(Alt.u2,Alt.v1); glVertex2f(X2+pxWidth/CELL_SIZE_PX,Y2-pxHeight/CELL_SIZE_PX); + glTexCoord2f(Alt.u1,Alt.v1); glVertex2f(X2 ,Y2-pxHeight/CELL_SIZE_PX); + glEnd; + TKMRender.BindTexture(0); + end; + end; + + glDisable(GL_ALPHA_TEST); + glAlphaFunc(GL_ALWAYS, 0); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Revert alpha mode + + glPopAttrib; + + glStencilFunc(GL_EQUAL, 1, 1); + glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); + glColorMask(True, True, True, True); + + // Render sprite + with gGFXData[aRX,aId] do + begin + // FOW is rendered over the top so no need to make sprites black anymore + glColor4ub(255, 255, 255, 255); + + TKMRender.BindTexture(Tex.TexID); + glBegin(GL_QUADS); + glTexCoord2f(Tex.u1,Tex.v2); glVertex2f(rX , rY ); + glTexCoord2f(Tex.u2,Tex.v2); glVertex2f(rX+pxWidth/CELL_SIZE_PX, rY ); + glTexCoord2f(Tex.u2,Tex.v1); glVertex2f(rX+pxWidth/CELL_SIZE_PX, rY-pxHeight/CELL_SIZE_PX); + glTexCoord2f(Tex.u1,Tex.v1); glVertex2f(rX , rY-pxHeight/CELL_SIZE_PX); + glEnd; + TKMRender.BindTexture(0); + end; + + glDisable(GL_STENCIL_TEST); +end; + + +procedure TKMRenderPool.CollectPlans(const aRect: TKMRect); +var + I: Integer; +begin + fFieldsList.Clear; + fHousePlansList.Clear; + + // Collect field plans (road, corn, wine) + if gGameParams.IsReplayOrSpectate then + begin + if gMySpectator.FOWIndex = -1 then + for I := 0 to gHands.Count - 1 do + // Don't use Hand.GetFieldPlans as it will give us plans multiple times for allies + gHands[I].Constructions.FieldworksList.GetFields(fFieldsList, aRect, False) + else + gHands[gMySpectator.FOWIndex].GetFieldPlans(fFieldsList, aRect, False); + end + else + begin + // Field plans for self and allies + // Include fake field plans for painting + gMySpectator.Hand.GetFieldPlans(fFieldsList, aRect, True); + end; + + // House plans for self and allies + if gGameParams.IsReplayOrSpectate then + begin + if gMySpectator.FOWIndex = -1 then + for I := 0 to gHands.Count - 1 do + // Don't use Hand.GetHousePlans as it will give us plans multiple times for allies + gHands[I].Constructions.HousePlanList.GetOutlines(fHousePlansList, aRect) + else + gHands[gMySpectator.FOWIndex].GetHousePlans(fHousePlansList, aRect); + end + else + gMySpectator.Hand.GetHousePlans(fHousePlansList, aRect); +end; + + +//Render wire on tile +//P - tile coords +//Col - Color +//aInset - Internal adjustment, to render wire "inside" tile +procedure TKMRenderPool.RenderWireTile(const P: TKMPoint; aCol: TColor4; aInset: Single = 0.0; aLineWidth: Single = -1); +begin + if not gTerrain.TileInMapCoords(P.X, P.Y) then Exit; + + TKMRender.BindTexture(0); // We have to reset texture to default (0), because it could be bind to any other texture (atlas) + + //Change LineWidth + if aLineWidth > 0 then + glLineWidth(aLineWidth); + + gRenderAux.RenderWireTile(P, aCol, aInset); + + if aLineWidth > 0 then + SetDefaultRenderParams; +end; + + +// Until profiling we use straightforward approach of recreating outline each frame +// Optimize later if needed +procedure TKMRenderPool.RenderHouseOutline(aHouseSketch: TKMHouseSketch; aCol: TColor4 = icCyan); +var + I: Integer; + loc: TKMPoint; + X, Y: Word; +begin + if (aHouseSketch = nil) or aHouseSketch.IsEmpty then + Exit; + + // Get an outline of build area + fHouseOutline.Clear; + + loc := aHouseSketch.Position; + gRes.Houses[aHouseSketch.HouseType].Outline(fHouseOutline); + + TKMRender.BindTexture(0); // We have to reset texture to default (0), because it could be bind to any other texture (atlas) +// glColor3f(0, 1, 1); + glColor4ubv(@aCol); + glBegin(GL_LINE_LOOP); + with gTerrain do + for I := 0 to fHouseOutline.Count - 1 do + begin + X := loc.X + fHouseOutline[I].X - 3; + Y := loc.Y + fHouseOutline[I].Y - 4; + glVertex2f(X, Y - LandExt^[Y+1, X+1].RenderHeight / CELL_HEIGHT_DIV); + end; + glEnd; +end; + + +procedure TKMRenderPool.RenderSpriteOnTile(const aLoc: TKMPoint; aId: Integer; aFlagColor: TColor4 = $FFFFFFFF); +var + pX, pY: Single; +begin + if not gTerrain.TileInMapCoords(aLoc.X, aLoc.Y) + or (gMySpectator.FogOfWar.CheckVerticeRenderRev(aLoc.X,aLoc.Y) <= FOG_OF_WAR_MIN) then Exit; + + pX := aLoc.X - 0.5 + fRXData[rxGui].Pivot[aId].X / CELL_SIZE_PX; + pY := gTerrain.RenderFlatToHeight(aLoc.X - 0.5, aLoc.Y - 0.5) - + fRXData[rxGui].Pivot[aId].Y / CELL_SIZE_PX; + RenderSprite(rxGui, aId, pX, pY, aFlagColor); +end; + + +procedure TKMRenderPool.RenderSpriteOnTerrain(const aLoc: TKMPointF; aId: Integer; aFlagColor: TColor4 = $FFFFFFFF; aForced: Boolean = False); +var + pX, pY: Single; +begin + // if not gTerrain.TileInMapCoords(aLoc.X, aLoc.Y) then Exit; + pX := aLoc.X + fRXData[rxGui].Pivot[aId].X / CELL_SIZE_PX; + pY := gTerrain.RenderFlatToHeight(aLoc.X, aLoc.Y) + + fRXData[rxGui].Pivot[aId].Y / CELL_SIZE_PX; + RenderSprite(rxGui, aId, pX, pY, aFlagColor, False, 0, aForced); +end; + + +procedure TKMRenderPool.RenderWireHousePlan(const P: TKMPoint; aHouseType: TKMHouseType); +var + I: Integer; + showHMarksIgnoreFOW: Boolean; +begin + fMarksList.Clear; + //Show house marks ignoring player FOW if we can see all map in replay/spec + showHMarksIgnoreFOW := gGameParams.IsReplayOrSpectate and (gMySpectator.FOWIndex = -1); + gMySpectator.Hand.GetHouseMarks(P, aHouseType, fMarksList, showHMarksIgnoreFOW); + + for I := 0 to fMarksList.Count - 1 do + if fMarksList.Tag[I] = TC_OUTLINE then + RenderWireTile(fMarksList[I], icCyan) // Cyan rect + else + RenderSpriteOnTile(fMarksList[I], fMarksList.Tag[I]); // Icon +end; + + +procedure TKMRenderPool.RenderForegroundUI_Markers; +var + P: TKMPoint; + house: TKMHouseWFlagPoint; +begin + P := gCursor.Cell; + case gCursor.Tag1 of + MARKER_REVEAL: begin + RenderSpriteOnTile(P, 394, gMySpectator.Hand.FlagColor); + gRenderAux.CircleOnTerrain(P.X-0.5, P.Y-0.5, + gCursor.MapEdSize, + gMySpectator.Hand.FlagColor and $10FFFFFF, + gMySpectator.Hand.FlagColor); + end; + MARKER_DEFENCE: begin + RenderSpriteOnTile(P, Ord(gCursor.MapEdDirection) + 510, gMySpectator.Hand.FlagColor); + case gCursor.MapEdDefPosGroupType of + gtMelee: RenderSpriteOnTile(P, 371, gMySpectator.Hand.FlagColor); + gtAntiHorse: RenderSpriteOnTile(P, 374, gMySpectator.Hand.FlagColor); + gtRanged: RenderSpriteOnTile(P, 376, gMySpectator.Hand.FlagColor); + gtMounted: RenderSpriteOnTile(P, 377, gMySpectator.Hand.FlagColor); + end; + if gCursor.MapEdDefPosType = dtBackLine then + RenderWireTile(P, icBlue, 0.1); + end; + MARKER_CENTERSCREEN: RenderSpriteOnTile(P, 391, gMySpectator.Hand.FlagColor); + MARKER_AISTART: RenderSpriteOnTile(P, 390, gMySpectator.Hand.FlagColor); + MARKER_RALLY_POINT: if gMySpectator.Selected is TKMHouseWFlagPoint then + begin + house := TKMHouseWFlagPoint(gMySpectator.Selected); + PaintFlagPoint(house.Entrance, P, gMySpectator.Hand.FlagColor, gRes.Houses[house.HouseType].FlagPointTexId, True, True); + end; + end; +end; + + +procedure TKMRenderPool.RenderForegroundUI_ElevateEqualize; +var + I, K: Integer; + tmp: Single; + rad, slope: Byte; + F: TKMPointF; +begin + F := gCursor.Float; + rad := gCursor.MapEdSize; + slope := gCursor.MapEdSlope; + for I := Max((Round(F.Y) - rad), 1) to Min((Round(F.Y) + rad), gTerrain.MapY -1) do + for K := Max((Round(F.X) - rad), 1) to Min((Round(F.X) + rad), gTerrain.MapX - 1) do + begin + case gCursor.MapEdShape of + hsCircle: tmp := 1 - GetLengthI(I - Round(F.Y), K - Round(F.X)) / rad; + hsSquare: tmp := 1 - Math.max(abs(I-Round(F.Y)), abs(K-Round(F.X))) / rad; + else tmp := 0; + end; + tmp := Power(Abs(tmp), (slope + 1) / 6) * Sign(tmp); // Modify slopes curve + tmp := EnsureRange(tmp * 2.5, 0, 1); // *2.5 makes dots more visible + gRenderAux.DotOnTerrain(K, I, $FF or (Round(tmp*255) shl 24)); + end; + case gCursor.MapEdShape of + hsCircle: gRenderAux.CircleOnTerrain(round(F.X), round(F.Y), rad, $00000000, $FFFFFFFF); + hsSquare: gRenderAux.SquareOnTerrain(round(F.X) - rad, round(F.Y) - rad, round(F.X + rad), round(F.Y) + rad, $FFFFFFFF); + end; +end; + + +procedure TKMRenderPool.RenderForegroundUI_ObjectsBrush; +var + I, K: Integer; + tmp: Single; + rad, slope: Byte; + F: TKMPointF; +begin + F := gCursor.Float; + rad := (gCursor.MapEdSize div 2) +1; + slope := gCursor.MapEdSlope; + for I := Max((Round(F.Y) - rad), 1) to Min((Round(F.Y) + rad), gTerrain.MapY -1) do + for K := Max((Round(F.X) - rad), 1) to Min((Round(F.X) + rad), gTerrain.MapX - 1) do + begin + case gCursor.MapEdShape of + hsCircle: tmp := 1 - GetLengthI(I - Round(F.Y), K - Round(F.X)) / rad; + hsSquare: tmp := 1 - Math.max(abs(I-Round(F.Y)), abs(K-Round(F.X))) / rad; + else tmp := 0; + end; + tmp := Power(Abs(tmp), (slope + 1) / 6) * Sign(tmp); // Modify slopes curve + tmp := EnsureRange(tmp * 2.5, 0, 1); // *2.5 makes dots more visible + gRenderAux.DotOnTerrain(K, I, $FF or (Round(tmp*255) shl 24)); + end; + case gCursor.MapEdShape of + hsCircle: gRenderAux.CircleOnTerrain(round(F.X), round(F.Y), rad, $00000000, $FFFFFFFF); + hsSquare: gRenderAux.SquareOnTerrain(round(F.X) - rad, round(F.Y) - rad, round(F.X + rad), round(F.Y) + rad, $FFFFFFFF); + end; +end; + + +procedure TKMRenderPool.RenderWireTileInt(const X,Y: Integer); +begin + RenderWireTile(KMPoint(X, Y), icLightCyan, 0, 0.3); +end; + + +procedure TKMRenderPool.RenderTileInt(const X, Y: Integer); +begin + if gCursor.MapEdSize = 0 then + // Brush size smaller than one cell + gRenderAux.DotOnTerrain(Round(gCursor.Float.X), Round(gCursor.Float.Y), $FF80FF80) + else + RenderTile(Combo[TKMTerrainKind(gCursor.Tag1), TKMTerrainKind(gCursor.Tag1),1],X,Y,0); +end; + + +procedure TKMRenderPool.RenderForegroundUI_Brush; +var + P, RP: TKMPoint; + size: Integer; + isSquare: Boolean; +begin + P := gCursor.Cell; + size := gCursor.MapEdSize; + isSquare := gCursor.MapEdShape = hsSquare; + if gCursor.MapEdUseMagicBrush then + IterateOverArea(P, size, isSquare, RenderWireTileInt) + else + if gCursor.Tag1 <> 0 then + begin + if SHOW_BRUSH_APPLY_AREA then + begin + RP := P; + if size = 0 then + RP := KMPoint(Round(gCursor.Float.X+1), Round(gCursor.Float.Y+1)); + IterateOverArea(RP, size, isSquare, RenderWireTileInt, True); // Render surrounding tiles, that will be fixed with transitions + end; + IterateOverArea(P, size, isSquare, RenderTileInt); + end; +end; + + +//Render tile owner layer +procedure TKMRenderPool.RenderTileOwnerLayer(const aRect: TKMRect); +var + I, K: Integer; + P: TKMPoint; +begin + for I := aRect.Top to aRect.Bottom do + for K := aRect.Left to aRect.Right do + begin + P := KMPoint(K, I); + if (gTerrain.Land^[I, K].TileOwner <> HAND_NONE) //owner is set for tile + and (gTerrain.TileIsCornField(P) // show only for corn + wine + roads + or gTerrain.TileIsWineField(P) + or (gTerrain.Land^[I, K].TileOverlay = toRoad)) then + RenderWireTile(P, gHands[gTerrain.Land^[I, K].TileOwner].FlagColor, 0.05); + end; +end; + + +//Render tiles grid layer +procedure TKMRenderPool.RenderTilesGrid(const aRect: TKMRect); +var + I, K: Integer; +begin + for I := aRect.Top to aRect.Bottom do + for K := aRect.Left to aRect.Right do + RenderWireTile(KMPoint(K, I), icDarkCyan, 0, 1); +end; + + +procedure TKMRenderPool.RenderForegroundUI; +var + P: TKMPoint; +begin + if gCursor.Cell.Y * gCursor.Cell.X = 0 then Exit; // Caused a rare crash + + TKMRender.BindTexture(0); // We have to reset texture to default (0), because it could be bind to any other texture (atlas) + + if gGameParams.IsMapEditor then + gGame.MapEditor.Paint(plCursors, KMRect(0,0,0,0)); + + P := gCursor.Cell; + + if (gCursor.Mode <> cmNone) and (gCursor.Mode <> cmHouses) and + (gMySpectator.FogOfWar.CheckTileRevelation(P.X, P.Y) = 0) then + RenderSpriteOnTile(P, TC_BLOCK) // Red X + else + + with gTerrain do + case gCursor.Mode of + cmNone: ; + cmErase: if not gGameParams.IsMapEditor then + begin + if ((gMySpectator.Hand.Constructions.FieldworksList.HasFakeField(P) <> ftNone) + or gMySpectator.Hand.Constructions.HousePlanList.HasPlan(P) + or (gMySpectator.Hand.HousesHitTest(P.X, P.Y) <> nil)) + then + RenderWireTile(P, icCyan) // Cyan quad + else + RenderSpriteOnTile(P, TC_BLOCK); // Red X + end; + cmRoad: if gMySpectator.Hand.CanAddFakeFieldPlan(P, ftRoad) and (gCursor.Tag1 <> Ord(cfmErase)) then + RenderWireTile(P, icCyan) // Cyan quad + else + RenderSpriteOnTile(P, TC_BLOCK); // Red X + cmField: if (gMySpectator.Hand.CanAddFakeFieldPlan(P, ftCorn) or (gGameParams.IsMapEditor and gTerrain.TileIsCornField(P))) + and (gCursor.Tag1 <> Ord(cfmErase)) then + RenderWireTile(P, icCyan) // Cyan quad + else + RenderSpriteOnTile(P, TC_BLOCK); // Red X + cmWine: if (gMySpectator.Hand.CanAddFakeFieldPlan(P, ftWine) or (gGameParams.IsMapEditor and gTerrain.TileIsWineField(P))) + and (gCursor.Tag1 <> Ord(cfmErase)) then + RenderWireTile(P, icCyan) // Cyan quad + else + RenderSpriteOnTile(P, TC_BLOCK); // Red X + cmHouses: RenderWireHousePlan(KMPointAdd(P, gCursor.DragOffset), TKMHouseType(gCursor.Tag1)); // Cyan quads and red Xs + cmBrush: RenderForegroundUI_Brush; + cmTiles: if gCursor.MapEdDir in [0..3] then + RenderTile(gCursor.Tag1, P.X, P.Y, gCursor.MapEdDir) + else + RenderTile(gCursor.Tag1, P.X, P.Y, (gTerrain.AnimStep div 5) mod 4); // Spin it slowly so player remembers it is on randomized + cmOverlays: begin + RenderWireTile(P, icCyan); + if gCursor.Tag1 > 0 then + RenderTile(TILE_OVERLAY_IDS[TKMTileOverlay(gCursor.Tag1)], P.X, P.Y, 0); + end; + cmObjects: begin + // If there's object below - paint it in Red + RenderMapElement(gTerrain.Land^[P.Y,P.X].Obj, gTerrain.AnimStep, P.X, P.Y, True, True); + RenderMapElement(gCursor.Tag1, gTerrain.AnimStep, P.X, P.Y, True); + end; + cmObjectsBrush: RenderForegroundUI_ObjectsBrush; + cmMagicWater: begin + if gTerrain.Land[P.Y, P.X].BaseLayer.Rotation+1 <=3 then + RenderTile(192, P.X, P.Y, gTerrain.Land[P.Y, P.X].BaseLayer.Rotation+1) + else + RenderTile(192, P.X, P.Y, 0); + RenderWireTile(P, icCyan); + end; + cmEyeDropper: RenderWireTile(P, icCyan); // Cyan quad + cmRotateTile: RenderWireTile(P, icCyan); // Cyan quad + cmElevate, + cmEqualize: RenderForegroundUI_ElevateEqualize; + cmConstHeight: RenderForegroundUI_ElevateEqualize; + cmUnits: RenderForegroundUI_Units; + cmMarkers: RenderForegroundUI_Markers; + cmPaintBucket: RenderForegroundUI_PaintBucket(ssShift in gCursor.SState); + cmUniversalEraser: RenderForegroundUI_UniversalEraser(ssShift in gCursor.SState); + end; + +end; + + +procedure TKMRenderPool.RenderUnit(U: TKMUnit; const P: TKMPoint; aFlagColor: Cardinal; aDoHighlight: Boolean; aHighlightColor: Cardinal); +begin + RenderUnit(U.UnitType, KMPointDir(P, U.Direction), U.AnimStep, aFlagColor, aDoHighlight, aHighlightColor); +end; + + +procedure TKMRenderPool.RenderUnit(aUnitType: TKMUnitType; const P: TKMPointDir; aAnimStep: Integer; aFlagColor: Cardinal; aDoHighlight: Boolean = False; aHighlightColor: Cardinal = 0); +begin + AddUnitWithDefaultArm(aUnitType, 0, uaWalk, P.Dir, aAnimStep, P.Loc.X + UNIT_OFF_X, P.Loc.Y + UNIT_OFF_Y, + aFlagColor, True, aDoHighlight, aHighlightColor); +end; + + +procedure TKMRenderPool.DoRenderGroup(aUnitType: TKMUnitType; aLoc: TKMPointDir; aMembersCnt, aUnitsPerRow: Integer; aHandColor: Cardinal); + + procedure PaintGroup; + var + I: Integer; + unitPos: TKMPointF; + newPos: TKMPoint; + doesFit: Boolean; + begin + //Paint virtual members in MapEd mode + for I := 1 to aMembersCnt - 1 do + begin + newPos := GetPositionInGroup2(aLoc.Loc.X, aLoc.Loc.Y, aLoc.Dir, I, aUnitsPerRow, gTerrain.MapX, gTerrain.MapY, doesFit); + if not doesFit then Continue; //Don't render units that are off the map in the map editor + unitPos.X := newPos.X + UNIT_OFF_X; //MapEd units don't have sliding + unitPos.Y := newPos.Y + UNIT_OFF_Y; + gRenderPool.AddUnit(aUnitType, 0, uaWalk, aLoc.Dir, UNIT_STILL_FRAMES[aLoc.Dir], 0.0, unitPos.X, unitPos.Y, aHandColor, True, True); + end; + end; + +begin + if TKMUnitGroup.IsFlagRenderBeforeUnit(aLoc.Dir) then + begin + PaintGroup; + RenderUnit(aUnitType, aLoc, UNIT_STILL_FRAMES[aLoc.Dir], aHandColor); + end else begin + RenderUnit(aUnitType, aLoc, UNIT_STILL_FRAMES[aLoc.Dir], aHandColor); + PaintGroup; + end; +end; + + +//Try to render Unit or Unit group. +//Return True, if succeeded +function TKMRenderPool.TryRenderUnitOrGroup(aEntity: TKMHandEntity; aUnitFilterFunc, aGroupFilterFunc: TBooleanFunc; + aUseGroupFlagColor, aDoHighlight: Boolean; aHandColor, aFlagColor: Cardinal; aHighlightColor: Cardinal = 0): Boolean; +var + U: TKMUnit; + G: TKMUnitGroup; + groupFlagColor: Cardinal; +begin + Result := False; + if aEntity.IsUnit then + begin + U := TKMUnit(aEntity); + if not Assigned(aUnitFilterFunc) or aUnitFilterFunc(aEntity) then + begin + RenderUnit(U, U.Position, aHandColor, aDoHighlight, aHighlightColor); + Result := True; + end; + end else + if aEntity.IsGroup then + begin + G := TKMUnitGroup(aEntity); + if not Assigned(aGroupFilterFunc) or aGroupFilterFunc(aEntity) then + begin + U := G.FlagBearer; + if aUseGroupFlagColor then + groupFlagColor := G.FlagColor + else + groupFlagColor := aFlagColor; + + if G.IsFlagRenderBeforeUnit(U.Direction) then + begin + G.PaintHighlighted(0.0, aHandColor, groupFlagColor, True, aDoHighlight, aHighlightColor); + RenderUnit(U, U.Position, aHandColor, aDoHighlight, aHighlightColor); + end else begin + RenderUnit(U, U.Position, aHandColor, aDoHighlight, aHighlightColor); + G.PaintHighlighted(0.0, aHandColor, groupFlagColor, True, aDoHighlight, aHighlightColor); + end; + Result := True; + end; + end; +end; + + +procedure TKMRenderPool.RenderForegroundUI_Units; +var + entity: TKMHandEntity; + P: TKMPoint; + dir : TKMDirection; + UT: TKMUnitType; + formation: TKMFormation; +begin + Assert(gGameParams.IsMapEditor); + + if gCursor.Tag1 = 255 then + begin + entity := gMySpectator.HitTestCursorWGroup(True); + TryRenderUnitOrGroup(entity, nil, nil, True, True, DELETE_COLOR, 0, DELETE_COLOR); + end + else + begin + UT := TKMUnitType(gCursor.Tag1); + dir := dirS; + + P := gCursor.Cell; + if gTerrain.CanPlaceUnit(P, UT) then + begin + if UT in UNITS_WARRIORS then + begin + gGame.MapEditor.DetermineGroupFormationAndDir(P, UNIT_TO_GROUP_TYPE[TKMUnitType(gCursor.Tag1)], formation, dir); + DoRenderGroup(UT, KMPointDir(P, dir), formation.NumUnits, formation.UnitsPerRow, gMySpectator.Hand.FlagColor); + end + else + AddUnitWithDefaultArm(UT, 0, uaWalk, dir, UNIT_STILL_FRAMES[dirS], P.X+UNIT_OFF_X, P.Y+UNIT_OFF_Y, gMySpectator.Hand.FlagColor, True); + end + else + RenderSpriteOnTile(P, TC_BLOCK); // Red X + end; +end; + + +procedure TKMRenderPool.RenderForegroundUI_UniversalEraser(aHighlightAll: Boolean); +var + entity: TKMHandEntity; + P: TKMPoint; + isRendered: Boolean; +begin + P := gCursor.Cell; + entity := gMySpectator.HitTestCursorWGroup(True); + + isRendered := TryRenderUnitOrGroup(entity, nil, nil, True, True, DELETE_COLOR, 0, DELETE_COLOR); + + if (entity is TKMHouse) then + begin + AddWholeHouse(TKMHouse(entity), gHands[entity.Owner].FlagColor, True, True, DELETE_COLOR); + isRendered := True; + end; + + // Terrain object found on the cell + if (aHighlightAll or not isRendered) and (gTerrain.Land^[P.Y,P.X].Obj <> OBJ_NONE) then + begin + RenderMapElement(gTerrain.Land^[P.Y,P.X].Obj, gTerrain.AnimStep, P.X, P.Y, True, True); + isRendered := True; + end; + + if (aHighlightAll or not isRendered) and + (((gTerrain.Land^[P.Y, P.X].TileOverlay <> toNone) + and (gTerrain.Land^[P.Y, P.X].TileLock = tlNone)) //Sometimes we can point road tile under the house, do not show Cyan quad then + or (gGame.MapEditor.LandMapEd^[P.Y, P.X].CornOrWine <> 0)) then + RenderWireTile(P, icCyan); // Cyan quad +end; + + +function TKMRenderPool.PaintBucket_GroupToRender(aGroup: TObject): Boolean; +begin + Result := (aGroup is TKMUnitGroup) and (TKMUnitGroup(aGroup).Owner <> gMySpectator.HandID); +end; + + +function TKMRenderPool.PaintBucket_UnitToRender(aUnit: TObject): Boolean; +begin + Result := (aUnit is TKMUnit) and not (aUnit is TKMUnitAnimal) and + (TKMUnit(aUnit).Owner <> gMySpectator.HandID); +end; + + +procedure TKMRenderPool.RenderForegroundUI_PaintBucket(aHighlightAll: Boolean); +var + entity: TKMHandEntity; + highlightColor: Cardinal; + P: TKMPoint; + isRendered: Boolean; +begin + P := gCursor.Cell; + highlightColor := MultiplyBrightnessByFactor(gMySpectator.Hand.FlagColor, 2, 0.3, 0.9); + entity := gMySpectator.HitTestCursorWGroup; + + isRendered := TryRenderUnitOrGroup(entity, PaintBucket_UnitToRender, PaintBucket_GroupToRender, + False, True, + gMySpectator.Hand.FlagColor, gMySpectator.Hand.FlagColor, highlightColor); + + if entity.IsHouse and (entity.Owner <> gMySpectator.HandID) then + begin + AddWholeHouse(TKMHouse(entity), gMySpectator.Hand.FlagColor, True, True, highlightColor); + isRendered := True; + end; + + if (aHighlightAll or not isRendered) and + (((gTerrain.Land^[P.Y, P.X].TileOverlay = toRoad) + and (gTerrain.Land^[P.Y, P.X].TileLock = tlNone)) //Sometimes we can point road tile under the house, do not show Cyan quad then + or (gGame.MapEditor.LandMapEd^[P.Y, P.X].CornOrWine <> 0)) + and (gTerrain.Land^[P.Y, P.X].TileOwner <> gMySpectator.HandID) then //Only if tile has other owner + RenderWireTile(P, icCyan); // Cyan quad +end; + + +{ TKMRenderList } +constructor TKMRenderList.Create; +begin + inherited; + + // Pre-allocate some space + SetLength(fRenderList, 512); + + fUnitsRXData := gRes.Sprites[rxUnits].RXData; +end; + + +destructor TKMRenderList.Destroy; +begin + SetLength(fRenderList, 0); + + inherited; +end; + + +function TKMRenderList.GetSelectionUID(const aCurPos: TKMPointF): Integer; +var + I, K: Integer; +begin + Result := -1; // Didn't hit anything + // Skip if cursor is over FOW + if gMySpectator.FogOfWar.CheckRevelation(aCurPos) <= FOG_OF_WAR_MIN then Exit; + // Select closest (higher Z) units first (list is in low..high Z-order) + for I := Length(fRenderOrder) - 1 downto 0 do + begin + K := fRenderOrder[I]; + // Don't check child sprites, we don't want to select serfs by the long pike they are carrying + if (fRenderList[K].UID > 0) and KMInRect(aCurPos, fRenderList[K].SelectionRect) then + begin + Result := fRenderList[K].UID; + Exit; + end; + end; +end; + + +procedure TKMRenderList.Clear; +begin + fCount := 0; +end; + + +procedure TKMRenderList.ClipRenderList; +var + I, J: Integer; +begin + SetLength(fRenderOrder, fCount); + J := 0; + for I := 0 to fCount - 1 do + if fRenderList[I].NewInst then + begin + fRenderOrder[J] := I; + Inc(J); + end; + SetLength(fRenderOrder, J); +end; + + +// Sort all items in list from top-right to bottom-left +procedure TKMRenderList.SortRenderList; +var + renderOrderAux: array of Word; + + procedure DoQuickSort(aLo, aHi: Integer); + var + lo, hi: Integer; + mid: Single; + begin + lo := aLo; + hi := aHi; + mid := fRenderList[fRenderOrder[(lo + hi) div 2]].Feet.Y; + repeat + while fRenderList[fRenderOrder[lo]].Feet.Y < mid do Inc(lo); + while fRenderList[fRenderOrder[hi]].Feet.Y > mid do Dec(hi); + if lo <= hi then + begin + SwapInt(fRenderOrder[lo], fRenderOrder[hi]); + Inc(lo); + Dec(hi); + end; + until lo > hi; + if hi > aLo then DoQuickSort(aLo, hi); + if lo < aHi then DoQuickSort(lo, aHi); + end; + + procedure Merge(aStart, aMid, aEnd: Integer); + var + I, A, B: Integer; + begin + A := aStart; + B := aMid; + for I := aStart to aEnd - 1 do + if (A < aMid) and ((B >= aEnd) + or (fRenderList[renderOrderAux[A]].Feet.Y <= fRenderList[renderOrderAux[B]].Feet.Y)) then + begin + fRenderOrder[I] := renderOrderAux[A]; + Inc(A); + end else begin + fRenderOrder[I] := renderOrderAux[B]; + Inc(B); + end; + end; + + // The same as Merge, but RenderOrder and RenderOrderAux are switched + procedure MergeAux(aStart, aMid, aEnd: Integer); + var + I, A, B: Integer; + begin + A := aStart; + B := aMid; + for I := aStart to aEnd-1 do + if (A < aMid) and ((B >= aEnd) + or (fRenderList[fRenderOrder[A]].Feet.Y <= fRenderList[fRenderOrder[B]].Feet.Y)) then + begin + renderOrderAux[I] := fRenderOrder[A]; + Inc(A); + end else begin + renderOrderAux[I] := fRenderOrder[B]; + Inc(B); + end; + end; + + // aUseAux tells us which array to store results in, it should flip each recurse + procedure DoMergeSort(aStart, aEnd: Integer; aUseAux: Boolean); + var + mid: Integer; + begin + if aEnd - aStart < 2 then Exit; + mid := (aStart + aEnd) div 2; + DoMergeSort(aStart, mid, not aUseAux); + DoMergeSort(mid, aEnd, not aUseAux); + if aUseAux then + MergeAux(aStart, mid, aEnd) + else + Merge(aStart, mid, aEnd); + end; + +begin + ClipRenderList; + if fCount > 0 then + begin + SetLength(renderOrderAux, Length(fRenderOrder)); + Move(fRenderOrder[0], renderOrderAux[0], Length(fRenderOrder)*SizeOf(fRenderOrder[0])); + // Quicksort is unstable which causes Z fighting, so we use mergesort + DoMergeSort(0, Length(fRenderOrder), False); + // DoQuickSort(0, Length(RenderOrder) - 1); + end; +end; + + +// New items must provide their ground level +procedure TKMRenderList.AddSpriteG(aRX: TRXType; aId: Integer; aUID: Integer; pX,pY,gX,gY: Single; aTeam: Cardinal = $0; aAlphaStep: Single = -1); +const + MAX_SEL_RECT_HEIGHT = 60; //Restrict too long images selection rect +var + hAdd, imH, hTop: Single; + snsTop, snsBottom: Integer; +begin + if fCount >= Length(fRenderList) then + SetLength(fRenderList, fCount + 256); // Book some space + + fRenderList[fCount].Loc := KMPointF(pX, pY); // Position of sprite, floating-point + fRenderList[fCount].Feet := KMPointF(gX, gY); // Ground position of sprite for Z-sorting + fRenderList[fCount].RX := aRX; // RX library + fRenderList[fCount].Id := aId; // Texture Id + fRenderList[fCount].UID := aUID; // Object Id + fRenderList[fCount].NewInst := True; // Is this a new item (can be occluded), or a child one (always on top of it's parent) + fRenderList[fCount].TeamColor := aTeam; // Team Id (determines color) + fRenderList[fCount].AlphaStep := aAlphaStep; // Alpha step for wip buildings + + if aUID > 0 then + with fRenderList[fCount].SelectionRect do + begin + snsTop := fUnitsRXData.SizeNoShadow[aId].Top; + snsBottom := fUnitsRXData.SizeNoShadow[aId].Bottom; + + imH := snsBottom - snsTop + 1; + hTop := EnsureRange(imH, CELL_SIZE_PX, MAX_SEL_RECT_HEIGHT); + + //Enlarge rect from image size to the top, to be at least CELL_SIZE_PX height + hAdd := Max(0, CELL_SIZE_PX - imH); // height to add to image pos. half to the top, half to the bottom + + Left := pX - 0.5 - fUnitsRXData.Pivot[aId].X / CELL_SIZE_PX; + Right := Left + 1; // Exactly +1 tile + Bottom := gY + ((hAdd / 2) - (fUnitsRXData.Size[aId].Y - snsBottom - 1))/ CELL_SIZE_PX; // Consider shadow at the image bottom + Top := Bottom - hTop / CELL_SIZE_PX; // -1 ~ -1.5 tiles + end; + + Inc(fCount); // New item added +end; + + +// Child items don't need ground level +procedure TKMRenderList.AddSprite(aRX: TRXType; aId: Integer; pX,pY: Single; aTeam: Cardinal = $0; aAlphaStep: Single = -1); +begin + if fCount >= Length(fRenderList) then + SetLength(fRenderList, fCount + 256); // Book some space + + fRenderList[fCount].Loc := KMPointF(pX,pY); // Position of sprite, floating-point + fRenderList[fCount].Feet := fRenderList[fCount-1].Feet; // Ground position of sprite for Z-sorting + fRenderList[fCount].RX := aRX; // RX library + fRenderList[fCount].Id := aId; // Texture Id + fRenderList[fCount].UID := 0; // Child sprites aren't used for selecting units + fRenderList[fCount].NewInst := False; // Is this a new item (can be occluded), or a child one (always on top of it's parent) + fRenderList[fCount].TeamColor := aTeam; // Team Id (determines color) + fRenderList[fCount].AlphaStep := aAlphaStep; // Alpha step for wip buildings + + Inc(fCount); // New item added +end; + + +procedure TKMRenderList.SendToRender(aId: Integer); +var + sp1, sp2: TKMRenderSprite; + sp2Exists: Boolean; +begin + // Shortcuts to Sprites info + sp1 := fRenderList[aId]; + sp2Exists := (aId + 1 < fCount); + if sp2Exists then + sp2 := fRenderList[aId + 1]; + + if sp1.AlphaStep = -1 then + gRenderPool.RenderSprite(sp1.RX, sp1.Id, sp1.Loc.X, sp1.Loc.Y, sp1.TeamColor) + else + begin + // Houses are rendered as Wood+Stone part. For Stone we want to skip + // Wooden part where it is occluded (so that smooth shadows dont overlay) + + // Check if next comes our child, Stone layer + if sp2Exists and not sp2.NewInst and (sp2.AlphaStep > 0) then + gRenderPool.RenderSpriteAlphaTest(sp1.RX, sp1.Id, sp1.AlphaStep, sp1.Loc.X, sp1.Loc.Y, + sp2.Id, sp2.AlphaStep, sp2.Loc.X, sp2.Loc.Y) + else + gRenderPool.RenderSpriteAlphaTest(sp1.RX, sp1.Id, sp1.AlphaStep, sp1.Loc.X, sp1.Loc.Y); + end; + +end; + + +// Now render all these items from list +procedure TKMRenderList.Render; +var + I, K, objectsCount: Integer; +begin + {$IFDEF DBG_PERFLOG} + gPerfLogs.SectionEnter(psFrameRenderList); + {$ENDIF} + + objectsCount := Length(fRenderOrder); + + for I := 0 to objectsCount - 1 do + begin + K := fRenderOrder[I]; + repeat // Render child sprites after their parent + SendToRender(K); + Inc(K); + until ((K = fCount) or fRenderList[K].NewInst); + end; + {$IFDEF DBG_PERFLOG} + gPerfLogs.SectionLeave(psFrameRenderList); + {$ENDIF} +end; + + +end. diff --git a/port-source/kam_remake/src/render/KM_RenderTerrain.pas b/port-source/kam_remake/src/render/KM_RenderTerrain.pas new file mode 100644 index 0000000..0b4e631 --- /dev/null +++ b/port-source/kam_remake/src/render/KM_RenderTerrain.pas @@ -0,0 +1,1376 @@ +unit KM_RenderTerrain; +{$I KaM_Remake.inc} +interface +uses + dglOpenGL, + SysUtils, Math, + KromUtils, + KM_ResSprites, KM_ResTilesetTypes, + KM_CommonClasses, KM_CommonTypes, KM_Defaults, KM_Points, + KM_FogOfWar, KM_Pics, + KM_TerrainTypes; + +type + TKMVBOArrayType = (vatNone, vatTile, vatTileLayer, vatAnimTile, vatFOW); + + TKMUVRect = array [1 .. 4, 1 .. 2] of Single; // Texture UV coordinates + + TKMTileVerticeExt = record + X, Y, Z, UTile, VTile, ULit, UShd: Single; + end; + + TKMTileFowVertice = record + X, Y, Z, UFow: Single; + end; + + TKMTileVertice = record + X, Y, Z, UAnim, VAnim: Single; + end; + + TKMTileVerticeExtArray = array of TKMTileVerticeExt; + + TKMTileFowVerticeArray = array of TKMTileFowVertice; + + TKMTileVerticeArray = array of TKMTileVertice; + + //Render terrain without sprites + TKMRenderTerrain = class + private + fClipRect: TKMRect; + fTextG: GLuint; //Shading gradient for lighting + fTextB: GLuint; //Contrast BW for FOW over color-coder + fUseVBO: Boolean; //Wherever to render terrain through VBO (faster but needs GL1.5) or DrawCalls (slower but needs only GL1.1) + + fTilesVtx: TKMTileVerticeExtArray; //Vertice buffer for tiles + fTilesVtxCount: Integer; + fTilesInd: array of Integer; //Indexes for tiles array + fTilesIndCount: Integer; + + fTilesLayersVtx: array of TKMTileVertice; //Vertice cache for layers + fTilesLayersInd: array of Integer; //Indexes for layers array + + fAnimTilesVtx: TKMTileVerticeArray; //Vertice buffer for tiles animations (water/falls/swamp) + fAnimTilesVtxCount: Integer; + fAnimTilesInd: array of Integer; //Indexes for array tiles animation array + fAnimTilesIndCount: Integer; + + fTilesFowVtx: TKMTileFowVerticeArray; //Vertice buffer for tiles + fTilesFowVtxCount: Integer; + fTilesFowInd: array of Integer; //Indexes for tiles array + fTilesFowIndCount: Integer; + + fVtxTilesShd: GLUint; + fIndTilesShd: GLUint; + fVtxTilesLayersShd: GLUint; + fIndTilesLayersShd: GLUint; + fVtxAnimTilesShd: GLUint; + fIndAnimTilesShd: GLUint; + fVtxTilesFowShd: GLUint; + fIndTilesFowShd: GLUint; + fTileUVLookup: array [0..TILES_CNT-1, 0..3] of TKMUVRect; + fLastBindVBOArrayType: TKMVBOArrayType; + fVBONeedsFlush: array [TKMVBOArrayType] of Boolean; + fVBOLastClipRect: TKMRect; + fVBOLastGameTick: Cardinal; + fVBOLastFOW: TKMFogOfWarCommon; + + procedure RenderQuadTexture(var TexC: TKMUVRect; tX,tY: Word); inline; + procedure RenderQuadTextureBlended(var TexC: TKMUVRect; tX,tY: Word; aCorners: TKMTileCorners; aBlendingLevel: Byte); inline; + + function GetTileUV(Index: Word; Rot: Byte): TKMUVRect; inline; + procedure BindVBOArray(aVBOArrayType: TKMVBOArrayType); inline; + procedure UpdateVBO(aAnimStep: Integer; aFOW: TKMFogOfWarCommon); + procedure DoTiles(aFOW: TKMFogOfWarCommon); + procedure DoTilesLayers(aFOW: TKMFogOfWarCommon); + procedure DoOverlays(aFOW: TKMFogOfWarCommon); + procedure DoLighting(aFOW: TKMFogOfWarCommon); + procedure DoAnimations(aAnimStep: Integer; aFOW: TKMFogOfWarCommon); + procedure DoShadows(aFOW: TKMFogOfWarCommon); + function VBOSupported: Boolean; + procedure RenderFence(aFence: TKMFenceKind; Pos: TKMDirection; pX,pY: Integer); + procedure RenderMarkup(pX, pY: Word; aFieldType: TKMFieldType); + procedure DoRenderTile(aTerrainId: Word; pX,pY,Rot: Integer; aDoBindTexture: Boolean; aUseTileLookup: Boolean; + DoHighlight: Boolean = False; HighlightColor: Cardinal = 0; aBlendingLvl: Byte = 0); overload; + procedure DoRenderTile(aTerrainId: Word; pX,pY,Rot: Integer; aCorners: TKMTileCorners; aDoBindTexture: Boolean; + aUseTileLookup: Boolean; DoHighlight: Boolean = False; HighlightColor: Cardinal = 0; + aBlendingLvl: Byte = 0); overload; + function DoUseVBO: Boolean; + public + constructor Create; + destructor Destroy; override; + property ClipRect: TKMRect read fClipRect write fClipRect; + procedure RenderBase(aAnimStep: Integer; aFOW: TKMFogOfWarCommon); + procedure RenderFences(aFOW: TKMFogOfWarCommon); + procedure RenderPlayerPlans(aFieldsList: TKMPointTagList; aHousePlansList: TKMPointDirList); + procedure RenderFOW(aFOW: TKMFogOfWarCommon; aUseContrast: Boolean = False); + procedure RenderTile(aTerrainId: Word; pX,pY,Rot: Integer; DoHighlight: Boolean = False; HighlightColor: Cardinal = 0); overload; + procedure RenderTile(pX,pY: Integer; aTileBasic: TKMTerrainTileBasic; DoHighlight: Boolean = False; HighlightColor: Cardinal = 0); overload; + procedure RenderTileOverlay(pX, pY: Integer; DoHighlight: Boolean = False; HighlightColor: Cardinal = 0); + end; + + +implementation +uses + KM_Game, + KM_GameParams, + KM_Render, KM_RenderTypes, + KM_Resource, KM_ResTypes, + KM_DevPerfLog, KM_DevPerfLogTypes, + KM_Terrain; + +const + TILE_LAYERS_USE_VBO = False; + MAX_RENDERABLE_TILES = (MAX_MAP_SIZE + 1) * (MAX_MAP_SIZE + 1); + MAX_RENDERABLE_VERTICIES = 4 * MAX_RENDERABLE_TILES; + MAX_RENDERABLE_INDEXES = 6 * MAX_RENDERABLE_TILES; + + +{ TKMRenderTerrain } +constructor TKMRenderTerrain.Create; +var + I, K: Integer; + pData: array [0..255] of Cardinal; + V: TKMVBOArrayType; +begin + inherited; + //Tiles UV lookup for faster access. Only base tileset for smaller size + for I := 0 to TILES_CNT - 1 do + for K := 0 to 3 do + fTileUVLookup[I, K] := GetTileUV(I, K); + + //Generate gradient programmatically + //KaM uses [0..255] gradients + //We use slightly smoothed gradients [16..255] for Remake + //cos it shows much more of terrain on screen and it looks too contrast + for I := 0 to 255 do + pData[I] := EnsureRange(Round(I * 1.0625 - 16), 0, 255) * 65793 or $FF000000; + + fTextG := TKMRender.GenTexture(256, 1, @pData[0], tfRGBA8, ftNearest, ftNearest); + + //Sharp transition between black and white + pData[0] := $FF000000; + pData[1] := $00000000; + pData[2] := $00000000; + pData[3] := $00000000; + fTextB := TKMRender.GenTexture(4, 1, @pData[0], tfRGBA8, ftNearest, ftNearest); + + fUseVBO := DoUseVBO; + + if fUseVBO then + begin + glGenBuffers(1, @fVtxTilesShd); + glGenBuffers(1, @fIndTilesShd); + glGenBuffers(1, @fVtxTilesLayersShd); + glGenBuffers(1, @fIndTilesLayersShd); + glGenBuffers(1, @fVtxAnimTilesShd); + glGenBuffers(1, @fIndAnimTilesShd); + glGenBuffers(1, @fVtxTilesFowShd); + glGenBuffers(1, @fIndTilesFowShd); + + //Allocate buffers large enough for the entire map + SetLength(fTilesVtx, MAX_RENDERABLE_VERTICIES); + SetLength(fTilesInd, MAX_RENDERABLE_INDEXES); + SetLength(fTilesFowVtx, MAX_RENDERABLE_VERTICIES); + SetLength(fTilesFowInd, MAX_RENDERABLE_INDEXES); + SetLength(fAnimTilesVtx, MAX_RENDERABLE_VERTICIES); + SetLength(fAnimTilesInd, MAX_RENDERABLE_INDEXES); + + fTilesVtxCount := 0; + fTilesIndCount := 0; + fTilesFowVtxCount := 0; + fTilesFowIndCount := 0; + fAnimTilesVtxCount := 0; + fAnimTilesIndCount := 0; + end; + + for V := Low(TKMVBOArrayType) to High(TKMVBOArrayType) do + fVBONeedsFlush[V] := True; +end; + + +destructor TKMRenderTerrain.Destroy; +begin +// fUseVBO := VBOSupported; //Could have been set to False if 3D rendering is enabled, so reset it + if fUseVBO then + begin + //Since RenderTerrain is created fresh everytime fGame is created, we should clear + //the buffers to avoid memory leaks. + glDeleteBuffers(1, @fVtxTilesShd); + glDeleteBuffers(1, @fIndTilesShd); + glDeleteBuffers(1, @fVtxTilesLayersShd); + glDeleteBuffers(1, @fIndTilesLayersShd); + glDeleteBuffers(1, @fVtxAnimTilesShd); + glDeleteBuffers(1, @fIndAnimTilesShd); + glDeleteBuffers(1, @fVtxTilesFowShd); + glDeleteBuffers(1, @fIndTilesFowShd); + end; + + inherited; +end; + + +function TKMRenderTerrain.VBOSupported: Boolean; +begin + //Some GPUs don't comply with OpenGL 1.5 spec on VBOs, so check Assigned instead of GL_VERSION_1_5 + Result := Assigned(glGenBuffers) and Assigned(glBindBuffer) and Assigned(glBufferData) and + Assigned(glEnableClientState) and Assigned(glVertexPointer) and Assigned(glClientActiveTexture) and + Assigned(glTexCoordPointer) and Assigned(glDrawElements) and Assigned(glDisableClientState) and + Assigned(glDeleteBuffers); +end; + + +function TKMRenderTerrain.DoUseVBO: Boolean; +begin + Result := VBOSupported; +end; + + +function TKMRenderTerrain.GetTileUV(Index: Word; Rot: Byte): TKMUVRect; +var + TexO: array [1 .. 4] of Byte; // order of UV coordinates, for rotations + A: Byte; +begin + TexO[1] := 1; + TexO[2] := 2; + TexO[3] := 3; + TexO[4] := 4; + + // Rotate by 90 degrees: 4-1-2-3 + if Rot and 1 = 1 then + begin + A := TexO[4]; + TexO[4] := TexO[3]; + TexO[3] := TexO[2]; + TexO[2] := TexO[1]; + TexO[1] := A; + end; + + //Rotate by 180 degrees: 3-4-1-2 + if Rot and 2 = 2 then + begin + SwapInt(TexO[1], TexO[3]); + SwapInt(TexO[2], TexO[4]); + end; + + // Rotate by 270 degrees = 90 + 180 + + //Apply rotation + with gGFXData[rxTiles, Index+1] do + begin + Result[TexO[1], 1] := Tex.u1; Result[TexO[1], 2] := Tex.v1; + Result[TexO[2], 1] := Tex.u1; Result[TexO[2], 2] := Tex.v2; + Result[TexO[3], 1] := Tex.u2; Result[TexO[3], 2] := Tex.v2; + Result[TexO[4], 1] := Tex.u2; Result[TexO[4], 2] := Tex.v1; + end; +end; + + +function TileHasToBeRendered(IsFirst: Boolean; aTX,aTY: Word; aFOW: TKMFogOfWarCommon): Boolean; inline; +begin + // We have to render at least 1 tile (otherwise smth wrong with gl contex and all UI and other sprites are not rendered at all + // so lets take the 1st tile + Result := IsFirst or (aFOW.CheckVerticeRenderRev(aTX,aTY) > FOG_OF_WAR_MIN); +end; + + +procedure TKMRenderTerrain.UpdateVBO(aAnimStep: Integer; aFOW: TKMFogOfWarCommon); +var + fog: PKMByte2Array; + + procedure SetTileVertexExt(out aVert: TKMTileVerticeExt; aTX, aTY: Word; + aIsBottomRow: Boolean; aUTile, aVTile: Single); inline; + begin + with gTerrain do + begin + aVert.X := aTX; + aVert.Y := aTY - LandExt^[aTY+1, aTX+1].RenderHeight / CELL_HEIGHT_DIV; + aVert.Z := aTY - Byte(aIsBottomRow); + aVert.UTile := aUTile; + aVert.VTile := aVTile; + aVert.ULit := LandExt[aTY+1, aTX+1].RenderLight; + aVert.UShd := -LandExt[aTY+1, aTX+1].RenderLight; + end; + end; + + procedure SetTileFowVertex(out aVert: TKMTileFowVertice; Fog: PKMByte2Array; aTX, aTY: Word; aIsBottomRow: Boolean); inline; + begin + aVert.X := aTX; + aVert.Y := aTY - gTerrain.LandExt^[aTY+1, aTX+1].RenderHeight / CELL_HEIGHT_DIV; + aVert.Z := aTY - Byte(aIsBottomRow); + if Fog <> nil then + aVert.UFow := Fog^[aTY, aTX] / 256 + else + aVert.UFow := 255; + end; + + procedure SetTileVertex(out aVert: TKMTileVertice; aTX, aTY: Word; aIsBottomRow: Boolean; aUAnimTile, aVAnimTile: Single); inline; + begin + with gTerrain do + begin + aVert.X := aTX; + aVert.Y := aTY - LandExt^[aTY+1, aTX+1].RenderHeight / CELL_HEIGHT_DIV; + aVert.Z := aTY - Byte(aIsBottomRow); + aVert.UAnim := aUAnimTile; + aVert.VAnim := aVAnimTile; + end; + end; + + function TryAddAnimTex(var aAnimCnt: Integer; aTX, aTY: Word; aAnimStep: Integer): Boolean; + + function SetAnimTileVertex(aTerrain: Word; aRotation: Byte): Boolean; + var + I: Integer; + texAnimC: TKMUVRect; + vtxOffset, indOffset: Integer; + tile: TKMTileParams; + begin + tile := gRes.Tileset[aTerrain]; + if not tile.HasAnim then Exit(False); + + Result := False; + + for I := Low(tile.Animation.Layers) to High(tile.Animation.Layers) do + begin + if not tile.Animation.Layers[I].HasAnim then Continue; + + texAnimC := GetTileUV(tile.Animation.Layers[I].GetAnim(aAnimStep), aRotation mod 4); + + vtxOffset := aAnimCnt * 4; + indOffset := aAnimCnt * 6; + + SetTileVertex(fAnimTilesVtx[vtxOffset], aTX-1, aTY-1, False, texAnimC[1][1], texAnimC[1][2]); + SetTileVertex(fAnimTilesVtx[vtxOffset+1], aTX-1, aTY, True, texAnimC[2][1], texAnimC[2][2]); + SetTileVertex(fAnimTilesVtx[vtxOffset+2], aTX, aTY, True, texAnimC[3][1], texAnimC[3][2]); + SetTileVertex(fAnimTilesVtx[vtxOffset+3], aTX, aTY-1, False, texAnimC[4][1], texAnimC[4][2]); + + fAnimTilesInd[indOffset+0] := vtxOffset; + fAnimTilesInd[indOffset+1] := vtxOffset + 1; + fAnimTilesInd[indOffset+2] := vtxOffset + 2; + fAnimTilesInd[indOffset+3] := vtxOffset; + fAnimTilesInd[indOffset+4] := vtxOffset + 3; + fAnimTilesInd[indOffset+5] := vtxOffset + 2; + + Inc(aAnimCnt); + Result := True; + end; + end; + + var + L: Integer; + begin + Result := SetAnimTileVertex(gTerrain.Land^[aTY,aTX].BaseLayer.Terrain, gTerrain.Land^[aTY,aTX].BaseLayer.Rotation); + for L := 0 to gTerrain.Land^[aTY,aTX].LayersCnt - 1 do + if not Result then + Result := SetAnimTileVertex(BASE_TERRAIN[gRes.Sprites.GetGenTerrainInfo(gTerrain.Land^[aTY,aTX].Layer[L].Terrain).TerKind], + gTerrain.Land^[aTY,aTX].Layer[L].Rotation) + else + Exit; + end; + +var + I, J, tilesCnt, fowCnt, animCnt, vtxOffset, indOffset: Integer; +// P,L,TilesLayersCnt: Integer; + sizeX, sizeY: Word; + tX, tY: Word; + texTileC: TKMUVRect; + V: TKMVBOArrayType; +begin + if not fUseVBO then Exit; + + //Skip updating VBOs if GameTick and ClipRect haven't changed + if not gGameParams.IsMapEditor + and (fClipRect = fVBOLastClipRect) + and (fVBOLastFOW = aFOW) + and (gGameParams.Tick = fVBOLastGameTick) then + Exit; + {$IFDEF DBG_PERFLOG} + gPerfLogs.SectionEnter(psFrameUpdateVBO); + {$ENDIF} + + fVBOLastClipRect := fClipRect; + fVBOLastGameTick := gGameParams.Tick; + fVBOLastFOW := aFOW; + + fLastBindVBOArrayType := vatNone; + + if aFOW is TKMFogOfWar then + fog := @TKMFogOfWar(aFOW).Revelation + else + fog := nil; + + sizeX := Max(fClipRect.Right - fClipRect.Left, 0); + sizeY := Max(fClipRect.Bottom - fClipRect.Top, 0); + + tilesCnt := 0; + fowCnt := 0; + animCnt := 0; +// P := 0; +// SetLength(fTilesLayersVtx, (SizeX + 1) * 4 * 3 * (SizeY + 1)); + + with gTerrain do + if (MapX > 0) and (MapY > 0) then + for I := 0 to sizeY do + for J := 0 to sizeX do + begin + tX := J + fClipRect.Left; + tY := I + fClipRect.Top; + + if TileHasToBeRendered(I*J = 0,tX,tY,aFow) then // Do not render tiles fully covered by FOW + begin + texTileC := fTileUVLookup[Land^[tY, tX].BaseLayer.Terrain, Land^[tY, tX].BaseLayer.Rotation mod 4]; + + vtxOffset := tilesCnt * 4; + indOffset := tilesCnt * 6; + + //Fill Tile vertices array + SetTileVertexExt(fTilesVtx[vtxOffset], tX-1, tY-1, False, texTileC[1][1], texTileC[1][2]); + SetTileVertexExt(fTilesVtx[vtxOffset+1], tX-1, tY, True, texTileC[2][1], texTileC[2][2]); + SetTileVertexExt(fTilesVtx[vtxOffset+2], tX, tY, True, texTileC[3][1], texTileC[3][2]); + SetTileVertexExt(fTilesVtx[vtxOffset+3], tX, tY-1, False, texTileC[4][1], texTileC[4][2]); + + // Set Tile terrain indices + fTilesInd[indOffset+0] := vtxOffset; + fTilesInd[indOffset+1] := vtxOffset + 1; + fTilesInd[indOffset+2] := vtxOffset + 2; + fTilesInd[indOffset+3] := vtxOffset; + fTilesInd[indOffset+4] := vtxOffset + 3; + fTilesInd[indOffset+5] := vtxOffset + 2; + + Inc(tilesCnt); + +// if Land^[tY, tX].LayersCnt > 0 then +// for L := 0 to Land^[tY, tX].LayersCnt - 1 do +// begin +// TexTileC := GetTileUV(Land[tY,tX].Layer[L].Terrain, Land^[TY,TX].Layer[L].Rotation mod 4); +// +// //Fill Tile vertices array +// SetTileVertex(fTilesLayersVtx, P, tX-1, tY-1, False, TexTileC[1][1], TexTileC[1][2]); +// SetTileVertex(fTilesLayersVtx, P+1, tX-1, tY, True, TexTileC[2][1], TexTileC[2][2]); +// SetTileVertex(fTilesLayersVtx, P+2, tX, tY, True, TexTileC[3][1], TexTileC[3][2]); +// SetTileVertex(fTilesLayersVtx, P+3, tX, tY-1, False, TexTileC[4][1], TexTileC[4][2]); +// P := P + 4; +// end; +// +// if gTerrain.Land^[tY, tX].LayersCnt > 0 then +// // Set Tile layers terrain indices +// for L := 0 to gTerrain.Land^[tY, tX].LayersCnt - 1 do +// begin +// fTilesLayersInd[P+0] := KP shl 2; // shl 2 = *4 +// fTilesLayersInd[P+1] := (KP shl 2) + 1; +// fTilesLayersInd[P+2] := (KP shl 2) + 2; +// fTilesLayersInd[P+3] := (KP shl 2); +// fTilesLayersInd[P+4] := (KP shl 2) + 3; +// fTilesLayersInd[P+5] := (KP shl 2) + 2; +// P := P + 6; +// Inc(KP); +// end; + end; + + vtxOffset := fowCnt * 4; + indOffset := fowCnt * 6; + + // Always set FOW + SetTileFowVertex(fTilesFowVtx[vtxOffset], fog, tX-1, tY-1, False); + SetTileFowVertex(fTilesFowVtx[vtxOffset+1], fog, tX-1, tY, True); + SetTileFowVertex(fTilesFowVtx[vtxOffset+2], fog, tX, tY, True); + SetTileFowVertex(fTilesFowVtx[vtxOffset+3], fog, tX, tY-1, False); + + // Set FOW indices + fTilesFowInd[indOffset+0] := vtxOffset; + fTilesFowInd[indOffset+1] := vtxOffset + 1; + fTilesFowInd[indOffset+2] := vtxOffset + 2; + fTilesFowInd[indOffset+3] := vtxOffset; + fTilesFowInd[indOffset+4] := vtxOffset + 3; + fTilesFowInd[indOffset+5] := vtxOffset + 2; + + Inc(fowCnt); + + // Render animation only if tile is not covered by FOW + if (aFOW.CheckVerticeRenderRev(tX,tY) > FOG_OF_WAR_ACT) then + //every tile can have only 1 animation + TryAddAnimTex(animCnt, tX, tY, aAnimStep); + end; + + //Update vertex/index counts + fTilesVtxCount := 4*tilesCnt; + fTilesIndCount := 6*tilesCnt; + + fTilesFowVtxCount := 4*fowCnt; + fTilesFowIndCount := 6*fowCnt; + + fAnimTilesVtxCount := 4*animCnt; + fAnimTilesIndCount := 6*animCnt; + +// SetLength(fTilesLayersVtx, P); +// TilesLayersCnt := P div 4; +// SetLength(fTileslayersInd, TilesLayersCnt*6); + + for V := Low(TKMVBOArrayType) to High(TKMVBOArrayType) do + fVBONeedsFlush[V] := True; + + {$IFDEF DBG_PERFLOG} + gPerfLogs.SectionLeave(psFrameUpdateVBO); + {$ENDIF} +end; + + +procedure TKMRenderTerrain.RenderQuadTexture(var TexC: TKMUVRect; tX,tY: Word); +begin + with gTerrain do + begin + glTexCoord2fv(@TexC[1]); glVertex3f(tX-1,tY-1-LandExt^[tY, tX].RenderHeight / CELL_HEIGHT_DIV, tY-1); + glTexCoord2fv(@TexC[2]); glVertex3f(tX-1,tY -LandExt^[tY+1,tX].RenderHeight / CELL_HEIGHT_DIV, tY-1); + glTexCoord2fv(@TexC[3]); glVertex3f(tX ,tY -LandExt^[tY+1,tX+1].RenderHeight / CELL_HEIGHT_DIV, tY-1); + glTexCoord2fv(@TexC[4]); glVertex3f(tX ,tY-1-LandExt^[tY, tX+1].RenderHeight / CELL_HEIGHT_DIV, tY-1); + end; +end; + + +procedure TKMRenderTerrain.RenderQuadTextureBlended(var TexC: TKMUVRect; tX,tY: Word; aCorners: TKMTileCorners; aBlendingLevel: Byte); +var + blendFactor: Single; +begin + blendFactor := 1 - Max(0, Min(1, aBlendingLevel / TERRAIN_MAX_BLENDING_LEVEL)); + with gTerrain do + begin + if aCorners[0] then + glColor4f(1,1,1,1) + else + glColor4f(1,1,1,blendFactor); + glTexCoord2fv(@TexC[1]); glVertex3f(tX-1,tY-1-LandExt^[tY, tX].RenderHeight / CELL_HEIGHT_DIV, tY-1); + + if aCorners[3] then + glColor4f(1,1,1,1) + else + glColor4f(1,1,1,blendFactor); + glTexCoord2fv(@TexC[2]); glVertex3f(tX-1,tY -LandExt^[tY+1,tX].RenderHeight / CELL_HEIGHT_DIV, tY-1); + + if aCorners[2] then + glColor4f(1,1,1,1) + else + glColor4f(1,1,1,blendFactor); + glTexCoord2fv(@TexC[3]); glVertex3f(tX ,tY -LandExt^[tY+1,tX+1].RenderHeight / CELL_HEIGHT_DIV, tY-1); + + if aCorners[1] then + glColor4f(1,1,1,1) + else + glColor4f(1,1,1,blendFactor); + glTexCoord2fv(@TexC[4]); glVertex3f(tX ,tY-1-LandExt^[tY, tX+1].RenderHeight / CELL_HEIGHT_DIV, tY-1); + glColor4f(1,1,1,1); + end; +end; + + +procedure TKMRenderTerrain.DoTiles(aFOW: TKMFogOfWarCommon); +var + I, K: Integer; + texC: TKMUVRect; + sizeX, sizeY: Word; + tX, tY: Word; +begin + {$IFDEF DBG_PERFLOG} + gPerfLogs.SectionEnter(psFrameTiles); + {$ENDIF} + //First we render base layer, then we do animated layers for Water/Swamps/Waterfalls + //They all run at different speeds so we can't adjoin them in one layer + glColor4f(1,1,1,1); + //Draw with VBO only if all tiles are on the same texture + if fUseVBO and TKMResSprites.AllTilesOnOneAtlas then + begin + if fTilesVtxCount = 0 then Exit; //Nothing to render + BindVBOArray(vatTile); + //Bind to tiles texture. All tiles should be places in 1 atlas, + //so to get TexId we can use any of terrain tile Id (f.e. 1st) + TKMRender.BindTexture(gGFXData[rxTiles, 1].Tex.TexID); + + //Setup vertex and UV layout and offsets + glEnableClientState(GL_VERTEX_ARRAY); + glVertexPointer(3, GL_FLOAT, SizeOf(TKMTileVerticeExt), Pointer(0)); + glClientActiveTexture(GL_TEXTURE0); + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + glTexCoordPointer(2, GL_FLOAT, SizeOf(TKMTileVerticeExt), Pointer(12)); + + //Here and above OGL requests Pointer, but in fact it's just a number (offset within Array) + glDrawElements(GL_TRIANGLES, fTilesIndCount, GL_UNSIGNED_INT, Pointer(0)); + + glDisableClientState(GL_VERTEX_ARRAY); + glDisableClientState(GL_TEXTURE_COORD_ARRAY); + end + else + begin + sizeX := Max(fClipRect.Right - fClipRect.Left, 0); + sizeY := Max(fClipRect.Bottom - fClipRect.Top, 0); + with gTerrain do + for I := 0 to sizeY do + for K := 0 to sizeX do + begin + tX := K + fClipRect.Left; + tY := I + fClipRect.Top; + if TileHasToBeRendered(I*K = 0, tX, tY, aFow) then // Do not render tiles fully covered by FOW + begin + with Land^[tY,tX] do + begin + TKMRender.BindTexture(gGFXData[rxTiles, BaseLayer.Terrain+1].Tex.TexID); + glBegin(GL_TRIANGLE_FAN); + texC := fTileUVLookup[BaseLayer.Terrain, BaseLayer.Rotation mod 4]; + end; + + RenderQuadTexture(texC, tX, tY); + glEnd; + end; + end; + end; + {$IFDEF DBG_PERFLOG} + gPerfLogs.SectionLeave(psFrameTiles); + {$ENDIF} +end; + + +procedure TKMRenderTerrain.DoTilesLayers(aFOW: TKMFogOfWarCommon); +var + I, K, L: Integer; + texC: TKMUVRect; + sizeX, sizeY: Word; + tX, tY: Word; + terInfo: TKMGenTerrainInfo; +begin + {$IFDEF DBG_PERFLOG} + gPerfLogs.SectionEnter(psFrameTilesLayers); + {$ENDIF} + //First we render base layer, then we do animated layers for Water/Swamps/Waterfalls + //They all run at different speeds so we can't adjoin them in one layer + glColor4f(1,1,1,1); + //Draw with VBO only if all tiles are on the same texture + + //DONT USE VBO FOR LAYERS FOR NOW, SINCE WE CAN USE BLENDING HERE + if TILE_LAYERS_USE_VBO + and fUseVBO + and TKMResSprites.AllTilesOnOneAtlas then + begin + if Length(fTilesLayersVtx) = 0 then Exit; //Nothing to render + BindVBOArray(vatTileLayer); + //Bind to tiles texture. All tiles should be places in 1 atlas, + //so to get TexId we can use any of terrain tile Id (f.e. 1st) + TKMRender.BindTexture(gGFXData[rxTiles, 1].Tex.TexID); + + //Setup vertex and UV layout and offsets + glEnableClientState(GL_VERTEX_ARRAY); + glVertexPointer(3, GL_FLOAT, SizeOf(TKMTileVertice), Pointer(0)); + glClientActiveTexture(GL_TEXTURE0); + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + glTexCoordPointer(2, GL_FLOAT, SizeOf(TKMTileVertice), Pointer(12)); + + //Here and above OGL requests Pointer, but in fact it's just a number (offset within Array) + glDrawElements(GL_TRIANGLES, Length(fTilesLayersInd), GL_UNSIGNED_INT, Pointer(0)); + + glDisableClientState(GL_VERTEX_ARRAY); + glDisableClientState(GL_TEXTURE_COORD_ARRAY); + end + else + begin + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + sizeX := Max(fClipRect.Right - fClipRect.Left, 0); + sizeY := Max(fClipRect.Bottom - fClipRect.Top, 0); + with gTerrain do + for I := 0 to sizeY do + for K := 0 to sizeX do + begin + tX := K + fClipRect.Left; + tY := I + fClipRect.Top; + if TileHasToBeRendered(I*K = 0,tX,tY,aFow) then // Do not render tiles fully covered by FOW + for L := 0 to Land^[tY,tX].LayersCnt - 1 do + begin + with Land^[tY,tX] do + begin + TKMRender.BindTexture(gGFXData[rxTiles, Layer[L].Terrain+1].Tex.TexID); + glBegin(GL_TRIANGLE_FAN); + texC := GetTileUV(Layer[L].Terrain, Layer[L].Rotation); + terInfo := gRes.Sprites.GetGenTerrainInfo(Layer[L].Terrain); + + if terInfo.TerKind = tkCustom then + Exit; + + if BlendingLvl > 0 then + RenderQuadTextureBlended(texC, tX, tY, Layer[L].GetCorners, BlendingLvl) + else + RenderQuadTexture(texC, tX, tY); + glEnd; + end; + end; + end; + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); //Just in case... + end; + {$IFDEF DBG_PERFLOG} + gPerfLogs.SectionLeave(psFrameTilesLayers); + {$ENDIF} +end; + + +procedure TKMRenderTerrain.DoAnimations(aAnimStep: Integer; aFOW: TKMFogOfWarCommon); +var + I, J, K: Integer; + texC: TKMUVRect; + animID: Word; + tile: TKMTileParams; +begin + {$IFDEF DBG_PERFLOG} + gPerfLogs.SectionEnter(psFrameWater); + {$ENDIF} + + //First we render base layer, then we do animated layers for Water/Swamps/Waterfalls + //They all run at different speeds so we can't adjoin them in one layer + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + if fUseVBO and TKMResSprites.AllTilesOnOneAtlas then + begin + if fAnimTilesVtxCount = 0 then Exit; //There is no animation on map + BindVBOArray(vatAnimTile); + //Bind to tiles texture. All tiles should be placed in 1 atlas, + //so to get TexId we can use any of terrain tile Id (f.e. 1st) + TKMRender.BindTexture(gGFXData[rxTiles, 1].Tex.TexID); + + //Setup vertex and UV layout and offsets + glEnableClientState(GL_VERTEX_ARRAY); + glVertexPointer(3, GL_FLOAT, SizeOf(TKMTileVertice), Pointer(0)); + glClientActiveTexture(GL_TEXTURE0); + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + glTexCoordPointer(2, GL_FLOAT, SizeOf(TKMTileVertice), Pointer(12)); + + //Here and above OGL requests Pointer, but in fact it's just a number (offset within Array) + glDrawElements(GL_TRIANGLES, fAnimTilesIndCount, GL_UNSIGNED_INT, Pointer(0)); + + glDisableClientState(GL_VERTEX_ARRAY); + glDisableClientState(GL_TEXTURE_COORD_ARRAY); + end + else + begin + with gTerrain do + for I := fClipRect.Top to fClipRect.Bottom do + for K := fClipRect.Left to fClipRect.Right do + begin + tile := gRes.Tileset[Land^[I,K].BaseLayer.Terrain]; + if tile.HasAnim + and (aFOW.CheckVerticeRenderRev(K,I) > FOG_OF_WAR_ACT) then //No animation in FOW + begin + for J := Low(tile.Animation.Layers) to High(tile.Animation.Layers) do + begin + if not tile.Animation.Layers[J].HasAnim then Continue; + + animID := tile.Animation.Layers[J].GetAnim(aAnimStep); + TKMRender.BindTexture(gGFXData[rxTiles, animID + 1].Tex.TexID); + texC := GetTileUV(animID, Land^[I,K].BaseLayer.Rotation); + + glBegin(GL_TRIANGLE_FAN); + glColor4f(1,1,1,1); + RenderQuadTexture(texC, K, I); + glEnd; + end; + end; + end; + end; + {$IFDEF DBG_PERFLOG} + gPerfLogs.SectionLeave(psFrameWater); + {$ENDIF} +end; + + +//Render single tile overlay +procedure TKMRenderTerrain.RenderTileOverlay(pX, pY: Integer; DoHighlight: Boolean = False; HighlightColor: Cardinal = 0); +// 1 //Select road tile and rotation +// 8*2 //depending on surrounding tiles +// 4 //Bitfield +const + RoadsConnectivity: array [0..15, 1..2] of Byte = ( + (248,0), (248,0), (248,1), (250,3), + (248,0), (248,0), (250,0), (252,0), + (248,1), (250,2), (248,1), (252,3), + (250,1), (252,2), (252,1), (254,0)); +var + road, ID, rot: Byte; +begin + //Fake tiles for MapEd fields + if gGame.MapEditor <> nil then + case gGame.MapEditor.LandMapEd^[pY, pX].CornOrWine of + 1: RenderTile(gGame.MapEditor.LandMapEd^[pY, pX].CornOrWineTerrain, pX, pY, 0, DoHighlight, HighlightColor); // Corn + 2: RenderTile(WINE_TERRAIN_ID, pX, pY, 0, DoHighlight, HighlightColor); //Wine + end; + + if gTerrain.Land^[pY, pX].TileOverlay = toRoad then + begin + road := 0; + if (pY - 1 >= 1) then + road := road + Byte(gTerrain.Land^[pY - 1, pX].TileOverlay = toRoad) shl 0; + if (pX + 1 <= gTerrain.MapX - 1) then + road := road + Byte(gTerrain.Land^[pY, pX + 1].TileOverlay = toRoad) shl 1; + if (pY + 1 <= gTerrain.MapY - 1) then + road := road + Byte(gTerrain.Land^[pY + 1, pX].TileOverlay = toRoad) shl 2; + if (pX - 1 >= 1) then + road := road + Byte(gTerrain.Land^[pY, pX - 1].TileOverlay = toRoad) shl 3; + ID := RoadsConnectivity[road, 1]; + rot := RoadsConnectivity[road, 2]; + RenderTile(ID, pX, pY, rot, DoHighlight, HighlightColor); + end + else if gTerrain.Land^[pY, pX].TileOverlay <> toNone then + RenderTile(TILE_OVERLAY_IDS[gTerrain.Land^[pY, pX].TileOverlay], pX, pY, 0, DoHighlight, HighlightColor); +end; + + +procedure TKMRenderTerrain.DoOverlays(aFOW: TKMFogOfWarCommon); +var + I, K: Integer; +begin + {$IFDEF DBG_PERFLOG} + gPerfLogs.SectionEnter(psFrameOverlays); + {$ENDIF} + if not (mlOverlays in gGameParams.VisibleLayers) then + Exit; + + for I := fClipRect.Top to fClipRect.Bottom do + for K := fClipRect.Left to fClipRect.Right do + if TileHasToBeRendered(False,K,I,aFow) then + RenderTileOverlay(K, I); + + {$IFDEF DBG_PERFLOG} + gPerfLogs.SectionLeave(psFrameOverlays); + {$ENDIF} +end; + + +procedure TKMRenderTerrain.RenderFences(aFOW: TKMFogOfWarCommon); +var + I, K: Integer; +begin + if gGameParams.IsMapEditor and not (mlOverlays in gGameParams.VisibleLayers) then + Exit; + + with gTerrain do + for I := fClipRect.Top to fClipRect.Bottom do + for K := fClipRect.Left to fClipRect.Right do + begin + if TileHasToBeRendered(False,K,I,aFow) then + begin + if Fences[I,K].Side and 1 = 1 then + RenderFence(Fences[I,K].Kind, dirN, K, I); + if Fences[I,K].Side and 2 = 2 then + RenderFence(Fences[I,K].Kind, dirE, K, I); + if Fences[I,K].Side and 4 = 4 then + RenderFence(Fences[I,K].Kind, dirW, K, I); + if Fences[I,K].Side and 8 = 8 then + RenderFence(Fences[I,K].Kind, dirS, K, I); + end; + end; +end; + + +//Player markings should be always clearly visible to the player (thats why we render them ontop FOW) +procedure TKMRenderTerrain.RenderPlayerPlans(aFieldsList: TKMPointTagList; aHousePlansList: TKMPointDirList); +var + I: Integer; +begin + //Rope field marks + for I := 0 to aFieldsList.Count - 1 do + RenderMarkup(aFieldsList[I].X, aFieldsList[I].Y, TKMFieldType(aFieldsList.Tag[I])); + + //Rope outlines + for I := 0 to aHousePlansList.Count - 1 do + RenderFence(fncHousePlan, aHousePlansList[I].Dir, aHousePlansList[I].Loc.X, aHousePlansList[I].Loc.Y); +end; + + +procedure TKMRenderTerrain.DoLighting(aFOW: TKMFogOfWarCommon); +var + I, K: Integer; + sizeX, sizeY: Word; + tX, tY: Word; +begin + {$IFDEF DBG_PERFLOG} + gPerfLogs.SectionEnter(psFrameLighting); + {$ENDIF} + + glColor4f(1, 1, 1, 1); + //Render highlights + glBlendFunc(GL_DST_COLOR, GL_ONE); + TKMRender.BindTexture(fTextG); + + if fUseVBO then + begin + if fTilesVtxCount = 0 then Exit; //Nothing to render + BindVBOArray(vatTile); + //Setup vertex and UV layout and offsets + glEnableClientState(GL_VERTEX_ARRAY); + glVertexPointer(3, GL_FLOAT, SizeOf(TKMTileVerticeExt), Pointer(0)); + glClientActiveTexture(GL_TEXTURE0); + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + glTexCoordPointer(1, GL_FLOAT, SizeOf(TKMTileVerticeExt), Pointer(20)); + + //Here and above OGL requests Pointer, but in fact it's just a number (offset within Array) + glDrawElements(GL_TRIANGLES, fTilesIndCount, GL_UNSIGNED_INT, Pointer(0)); + + glDisableClientState(GL_VERTEX_ARRAY); + glDisableClientState(GL_TEXTURE_COORD_ARRAY); + end + else + begin + sizeX := Max(fClipRect.Right - fClipRect.Left, 0); + sizeY := Max(fClipRect.Bottom - fClipRect.Top, 0); + with gTerrain do + for I := 0 to sizeY do + for K := 0 to sizeX do + begin + tX := K + fClipRect.Left; + tY := I + fClipRect.Top; + if TileHasToBeRendered(I*K = 0,tX,tY,aFow) then // Do not render tiles fully covered by FOW + begin + glBegin(GL_TRIANGLE_FAN); + glTexCoord1f(LandExt[ tY, tX].RenderLight); glVertex3f(tX-1, tY-1 - LandExt^[ tY, tX].RenderHeight / CELL_HEIGHT_DIV, tY-1); + glTexCoord1f(LandExt[tY+1, tX].RenderLight); glVertex3f(tX-1, tY - LandExt^[tY+1, tX].RenderHeight / CELL_HEIGHT_DIV, tY-1); + glTexCoord1f(LandExt[tY+1, tX+1].RenderLight); glVertex3f( tX, tY - LandExt^[tY+1, tX+1].RenderHeight / CELL_HEIGHT_DIV, tY-1); + glTexCoord1f(LandExt[ tY, tX+1].RenderLight); glVertex3f( tX, tY-1 - LandExt^[ tY, tX+1].RenderHeight / CELL_HEIGHT_DIV, tY-1); + glEnd; + end; + end; + end; + {$IFDEF DBG_PERFLOG} + gPerfLogs.SectionLeave(psFrameLighting); + {$ENDIF} +end; + + +//Render shadows and FOW at once +procedure TKMRenderTerrain.DoShadows(aFOW: TKMFogOfWarCommon); +var + I, K: Integer; + sizeX, sizeY: Word; + tX, tY: Word; +begin + {$IFDEF DBG_PERFLOG} + gPerfLogs.SectionEnter(psFrameShadows); + {$ENDIF} + + glColor4f(1, 1, 1, 1); + glBlendFunc(GL_ZERO, GL_ONE_MINUS_SRC_COLOR); + TKMRender.BindTexture(fTextG); + + if fUseVBO then + begin + if fTilesVtxCount = 0 then Exit; //Nothing to render + BindVBOArray(vatTile); + //Setup vertex and UV layout and offsets + glEnableClientState(GL_VERTEX_ARRAY); + glVertexPointer(3, GL_FLOAT, SizeOf(TKMTileVerticeExt), Pointer(0)); + glClientActiveTexture(GL_TEXTURE0); + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + glTexCoordPointer(1, GL_FLOAT, SizeOf(TKMTileVerticeExt), Pointer(24)); + + //Here and above OGL requests Pointer, but in fact it's just a number (offset within Array) + glDrawElements(GL_TRIANGLES, fTilesIndCount, GL_UNSIGNED_INT, Pointer(0)); + + glDisableClientState(GL_VERTEX_ARRAY); + glDisableClientState(GL_TEXTURE_COORD_ARRAY); + end + else + begin + sizeX := Max(fClipRect.Right - fClipRect.Left, 0); + sizeY := Max(fClipRect.Bottom - fClipRect.Top, 0); + with gTerrain do + for I := 0 to sizeY do + for K := 0 to sizeX do + begin + tX := K + fClipRect.Left; + tY := I + fClipRect.Top; + if TileHasToBeRendered(I*K = 0,tX,tY,aFow) then // Do not render tiles fully covered by FOW + begin + glBegin(GL_TRIANGLE_FAN); + glTexCoord1f(-LandExt[ tY, tX].RenderLight); glVertex3f(tX-1, tY-1 - LandExt^[ tY, tX].RenderHeight / CELL_HEIGHT_DIV, tY-1); + glTexCoord1f(-LandExt[tY+1, tX].RenderLight); glVertex3f(tX-1, tY - LandExt^[tY+1, tX].RenderHeight / CELL_HEIGHT_DIV, tY-1); + glTexCoord1f(-LandExt[tY+1, tX+1].RenderLight); glVertex3f( tX, tY - LandExt^[tY+1, tX+1].RenderHeight / CELL_HEIGHT_DIV, tY-1); + glTexCoord1f(-LandExt[ tY, tX+1].RenderLight); glVertex3f( tX, tY-1 - LandExt^[ tY, tX+1].RenderHeight / CELL_HEIGHT_DIV, tY-1); + glEnd; + end; + end; + end; + + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + TKMRender.BindTexture(0); + + {$IFDEF DBG_PERFLOG} + gPerfLogs.SectionLeave(psFrameShadows); + {$ENDIF} +end; + + +//Render FOW at once +procedure TKMRenderTerrain.RenderFOW(aFOW: TKMFogOfWarCommon; aUseContrast: Boolean = False); +var + I, K: Integer; + fog: PKMByte2Array; +begin + if aFOW is TKMFogOfWarOpen then Exit; + {$IFDEF DBG_PERFLOG} + gPerfLogs.SectionEnter(psFrameFOW); + {$ENDIF} + + glColor4f(1, 1, 1, 1); + + if aUseContrast then + begin + //Hide everything behind FOW with a sharp transition + glColor4f(0, 0, 0, 1); + TKMRender.BindTexture(fTextB); + end + else + begin + glBlendFunc(GL_ZERO, GL_SRC_COLOR); + TKMRender.BindTexture(fTextG); + end; + + fog := @TKMFogOfWar(aFOW).Revelation; + if fUseVBO then + begin + if fTilesFowVtxCount = 0 then Exit; //Nothing to render + BindVBOArray(vatFOW); + + //Setup vertex and UV layout and offsets + glEnableClientState(GL_VERTEX_ARRAY); + glVertexPointer(3, GL_FLOAT, SizeOf(TKMTileFowVertice), Pointer(0)); + glClientActiveTexture(GL_TEXTURE0); + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + glTexCoordPointer(1, GL_FLOAT, SizeOf(TKMTileFowVertice), Pointer(12)); + + //Here and above OGL requests Pointer, but in fact it's just a number (offset within Array) + glDrawElements(GL_TRIANGLES, fTilesFowIndCount, GL_UNSIGNED_INT, Pointer(0)); + + glDisableClientState(GL_VERTEX_ARRAY); + glDisableClientState(GL_TEXTURE_COORD_ARRAY); + end + else + begin + with gTerrain do + for I := fClipRect.Top to fClipRect.Bottom do + for K := fClipRect.Left to fClipRect.Right do + begin + glBegin(GL_TRIANGLE_FAN); + glTexCoord1f(fog^[I - 1, K - 1] / 255); + glVertex2f(K - 1, I - 1 - LandExt^[I, K].RenderHeight / CELL_HEIGHT_DIV); + glTexCoord1f(fog^[I, K - 1] / 255); + glVertex2f(K - 1, I - LandExt^[I + 1, K].RenderHeight / CELL_HEIGHT_DIV); + glTexCoord1f(fog^[I, K] / 255); + glVertex2f(K, I - LandExt^[I + 1, K + 1].RenderHeight / CELL_HEIGHT_DIV); + glTexCoord1f(fog^[I - 1, K] / 255); + glVertex2f(K, I - 1 - LandExt^[I, K + 1].RenderHeight / CELL_HEIGHT_DIV); + glEnd; + end; + end; + + //Sprites (trees) can extend beyond the top edge of the map, so draw extra rows of fog to cover them + //@Krom: If you know a neater/faster way to solve this problem please feel free to change it. + //@Krom: Side note: Is it ok to not use VBOs in this case? (does mixing VBO with non-VBO code cause any problems?) + with gTerrain do + if fClipRect.Top <= 1 then + begin + //3 tiles is enough to cover the tallest tree with highest elevation on top row + for I := -2 to 0 do + for K := fClipRect.Left to fClipRect.Right do + begin + glBegin(GL_TRIANGLE_FAN); + glTexCoord1f(fog^[0, K - 1] / 255); + glVertex2f(K - 1, I - 1 - LandExt^[1, K].RenderHeight / CELL_HEIGHT_DIV); + glTexCoord1f(fog^[0, K - 1] / 255); + glVertex2f(K - 1, I - LandExt^[1, K].RenderHeight / CELL_HEIGHT_DIV); + glTexCoord1f(fog^[0, K] / 255); + glVertex2f(K, I - LandExt^[1, K + 1].RenderHeight / CELL_HEIGHT_DIV); + glTexCoord1f(fog^[0, K] / 255); + glVertex2f(K, I - 1 - LandExt^[1, K + 1].RenderHeight / CELL_HEIGHT_DIV); + glEnd; + end; + end; + //Similar thing for the bottom of the map (field borders can overhang) + with gTerrain do + if fClipRect.Bottom >= MapY-1 then + begin + //1 tile is enough to cover field borders + for K := fClipRect.Left to fClipRect.Right do + begin + glBegin(GL_TRIANGLE_FAN); + glTexCoord1f(fog^[MapY-1, K - 1] / 255); + glVertex2f(K - 1, MapY-1 - LandExt^[MapY, K].RenderHeight / CELL_HEIGHT_DIV); + glTexCoord1f(fog^[MapY-1, K - 1] / 255); + glVertex2f(K - 1, MapY - LandExt^[MapY, K].RenderHeight / CELL_HEIGHT_DIV); + glTexCoord1f(fog^[MapY-1, K] / 255); + glVertex2f(K, MapY-1 - LandExt^[MapY, K + 1].RenderHeight / CELL_HEIGHT_DIV); + glTexCoord1f(fog^[MapY-1, K] / 255); + glVertex2f(K, MapY - LandExt^[MapY, K + 1].RenderHeight / CELL_HEIGHT_DIV); + glEnd; + end; + end; + + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + TKMRender.BindTexture(0); + {$IFDEF DBG_PERFLOG} + gPerfLogs.SectionLeave(psFrameFOW); + {$ENDIF} +end; + + +procedure TKMRenderTerrain.BindVBOArray(aVBOArrayType: TKMVBOArrayType); +begin + if fLastBindVBOArrayType = aVBOArrayType then Exit; // Do not to rebind for same tyle type + + case aVBOArrayType of + vatTile: if fTilesVtxCount > 0 then + begin + glBindBuffer(GL_ARRAY_BUFFER, fVtxTilesShd); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, fIndTilesShd); + + if fVBONeedsFlush[aVBOArrayType] then + begin + glBufferData(GL_ARRAY_BUFFER, fTilesVtxCount * SizeOf(TKMTileVerticeExt), @fTilesVtx[0].X, GL_STREAM_DRAW); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, fTilesIndCount * SizeOf(fTilesInd[0]), @fTilesInd[0], GL_STREAM_DRAW); + fVBONeedsFlush[aVBOArrayType] := False; + end; + end else Exit; + vatTileLayer: if Length(fTilesLayersVtx) > 0 then + begin + glBindBuffer(GL_ARRAY_BUFFER, fVtxTilesLayersShd); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, fIndTilesLayersShd); + + if fVBONeedsFlush[aVBOArrayType] then + begin + glBufferData(GL_ARRAY_BUFFER, Length(fTilesLayersVtx) * SizeOf(TKMTileVertice), @fTilesLayersVtx[0].X, GL_STREAM_DRAW); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, Length(fTilesLayersInd) * SizeOf(fTilesLayersInd[0]), @fTilesLayersInd[0], GL_STREAM_DRAW); + fVBONeedsFlush[aVBOArrayType] := False; + end; + end else Exit; + vatAnimTile: if fAnimTilesVtxCount > 0 then + begin + glBindBuffer(GL_ARRAY_BUFFER, fVtxAnimTilesShd); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, fIndAnimTilesShd); + + if fVBONeedsFlush[aVBOArrayType] then + begin + glBufferData(GL_ARRAY_BUFFER, fAnimTilesVtxCount * SizeOf(TKMTileVertice), @fAnimTilesVtx[0].X, GL_STREAM_DRAW); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, fAnimTilesIndCount * SizeOf(fAnimTilesInd[0]), @fAnimTilesInd[0], GL_STREAM_DRAW); + fVBONeedsFlush[aVBOArrayType] := False; + end; + end else Exit; + vatFOW: if fTilesFowVtxCount > 0 then + begin + glBindBuffer(GL_ARRAY_BUFFER, fVtxTilesFowShd); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, fIndTilesFowShd); + + if fVBONeedsFlush[aVBOArrayType] then + begin + glBufferData(GL_ARRAY_BUFFER, fTilesFowVtxCount * SizeOf(TKMTileFowVertice), @fTilesFowVtx[0].X, GL_STREAM_DRAW); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, fTilesFowIndCount * SizeOf(fTilesFowInd[0]), @fTilesFowInd[0], GL_STREAM_DRAW); + fVBONeedsFlush[aVBOArrayType] := False; + end; + end else Exit; + end; + fLastBindVBOArrayType := aVBOArrayType; +end; + + +//AnimStep - animation step for terrain (water/etc) +//aFOW - whose players FOW to apply +procedure TKMRenderTerrain.RenderBase(aAnimStep: Integer; aFOW: TKMFogOfWarCommon); +begin + fUseVBO := DoUseVBO; + + UpdateVBO(aAnimStep, aFOW); + + {$IFDEF DBG_PERFLOG} + gPerfLogs.SectionEnter(psFrameTerrainBase); + {$ENDIF} + + DoTiles(aFOW); + //It was 'unlit water goes above lit sand' + //But there is no big difference there, that is why, to make possible transitions with water, + //Animations was put before DoLighting + DoAnimations(aAnimStep, aFOW); + //TileLayers after water, as water with animation is always base layer + DoTilesLayers(aFOW); + DoOverlays(aFOW); + DoLighting(aFOW); +// DoAnimations(aAnimStep, aFOW); + DoShadows(aFOW); + + {$IFDEF DBG_PERFLOG} + gPerfLogs.SectionLeave(psFrameTerrainBase); + {$ENDIF} +end; + + +procedure TKMRenderTerrain.DoRenderTile(aTerrainId: Word; pX,pY,Rot: Integer; aDoBindTexture: Boolean; aUseTileLookup: Boolean; + DoHighlight: Boolean = False; HighlightColor: Cardinal = 0; aBlendingLvl: Byte = 0); +const + NO_CORNERS: TKMTileCorners = (False, False, False, False); +begin + DoRenderTile(aTerrainId, pX,pY,Rot, NO_CORNERS, aDoBindTexture, aUseTileLookup, DoHighlight, HighlightColor, aBlendingLvl); +end; + + +procedure TKMRenderTerrain.DoRenderTile(aTerrainId: Word; pX,pY,Rot: Integer; aCorners: TKMTileCorners; aDoBindTexture: Boolean; + aUseTileLookup: Boolean; DoHighlight: Boolean = False; HighlightColor: Cardinal = 0; + aBlendingLvl: Byte = 0); +var + texC: TKMUVRect; // Texture UV coordinates +begin + if not gTerrain.TileInMapCoords(pX,pY) then Exit; + + if DoHighlight then + glColor4ub(HighlightColor and $FF, (HighlightColor shr 8) and $FF, (HighlightColor shr 16) and $FF, $FF) + else + glColor4f(1, 1, 1, 1); + + if aDoBindTexture then + TKMRender.BindTexture(gGFXData[rxTiles, aTerrainId + 1].Tex.TexID); + + if aUseTileLookup then + texC := fTileUVLookup[aTerrainId, Rot mod 4] + else + texC := GetTileUV(aTerrainId, Rot mod 4); + + glBegin(GL_TRIANGLE_FAN); + + //todo -cComplicated: DoHighlight and HighlightColor is not considered here, we use always glColor4f(1, 1, 1, 1); + if aBlendingLvl > 0 then + RenderQuadTextureBlended(texC, pX, pY, aCorners, aBlendingLvl) + else + RenderQuadTexture(texC, pX, pY); + + glEnd; +end; + + +//Render single terrain cell +procedure TKMRenderTerrain.RenderTile(aTerrainId: Word; pX,pY,Rot: Integer; DoHighlight: Boolean = False; HighlightColor: Cardinal = 0); +begin + DoRenderTile(aTerrainId, pX, pY, Rot, True, True, DoHighlight, HighlightColor); +end; + + +//Render single terrain cell +procedure TKMRenderTerrain.RenderTile(pX,pY: Integer; aTileBasic: TKMTerrainTileBasic; DoHighlight: Boolean = False; HighlightColor: Cardinal = 0); +var + L: Integer; + doBindTexture: Boolean; +begin + if not gTerrain.TileInMapCoords(pX,pY) then Exit; + + doBindTexture := not TKMResSprites.AllTilesOnOneAtlas; + if not doBindTexture then + TKMRender.BindTexture(gGFXData[rxTiles, aTileBasic.BaseLayer.Terrain + 1].Tex.TexID); + + // Render Base Layer + DoRenderTile(aTileBasic.BaseLayer.Terrain, pX, pY, aTileBasic.BaseLayer.Rotation, doBindTexture, + False, DoHighlight, HighlightColor); + + // Render other Layers + for L := 0 to aTileBasic.LayersCnt - 1 do + DoRenderTile(aTileBasic.Layer[L].Terrain, pX, pY, aTileBasic.Layer[L].Rotation, aTileBasic.Layer[L].GetCorners, + doBindTexture, False, DoHighlight, HighlightColor, aTileBasic.BlendingLvl); +end; + + +procedure TKMRenderTerrain.RenderFence(aFence: TKMFenceKind; Pos: TKMDirection; pX,pY: Integer); +const + FO = 4; //Fence overlap + VO = -4; //Move fences a little down to avoid visible overlap when unit stands behind fence, but is rendered ontop of it, due to Z sorting algo we use +var + UVa, UVb: TKMPointF; + texID: Integer; + x1, y1, y2, fenceX, fenceY: Single; + heightInPx: Integer; +begin + case aFence of + fncHouseFence: if Pos in [dirN,dirS] then texID:=463 else texID:=467; //WIP (Wood planks) + fncHousePlan: if Pos in [dirN,dirS] then texID:=105 else texID:=117; //Plan (Ropes) + fncWine: if Pos in [dirN,dirS] then texID:=462 else texID:=466; //Fence (Wood) + fncCorn: if Pos in [dirN,dirS] then texID:=461 else texID:=465; //Fence (Stones) + else texID := 0; + end; + + //With these directions render fences on next tile + if Pos = dirS then Inc(pY); + if Pos = dirW then Inc(pX); + + if Pos in [dirN, dirS] then + begin //Horizontal + TKMRender.BindTexture(gGFXData[rxGui,texID].Tex.TexID); + UVa.X := gGFXData[rxGui, texID].Tex.u1; + UVa.Y := gGFXData[rxGui, texID].Tex.v1; + UVb.X := gGFXData[rxGui, texID].Tex.u2; + UVb.Y := gGFXData[rxGui, texID].Tex.v2; + + y1 := pY - 1 - (gTerrain.LandExt^[pY, pX].RenderHeight + VO) / CELL_HEIGHT_DIV; + y2 := pY - 1 - (gTerrain.LandExt^[pY, pX + 1].RenderHeight + VO) / CELL_HEIGHT_DIV; + + fenceY := gGFXData[rxGui,texID].PxWidth / CELL_SIZE_PX; + glBegin(GL_QUADS); + glTexCoord2f(UVb.x, UVa.y); glVertex2f(pX-1 -3/ CELL_SIZE_PX, y1); + glTexCoord2f(UVa.x, UVa.y); glVertex2f(pX-1 -3/ CELL_SIZE_PX, y1 - fenceY); + glTexCoord2f(UVa.x, UVb.y); glVertex2f(pX +3/ CELL_SIZE_PX, y2 - fenceY); + glTexCoord2f(UVb.x, UVb.y); glVertex2f(pX +3/ CELL_SIZE_PX, y2); + glEnd; + end + else + begin //Vertical + TKMRender.BindTexture(gGFXData[rxGui,texID].Tex.TexID); + heightInPx := Round(CELL_SIZE_PX * (1 + (gTerrain.LandExt^[pY,pX].RenderHeight - gTerrain.LandExt^[pY+1,pX].RenderHeight)/CELL_HEIGHT_DIV)+FO); + UVa.X := gGFXData[rxGui, texID].Tex.u1; + UVa.Y := gGFXData[rxGui, texID].Tex.v1; + UVb.X := gGFXData[rxGui, texID].Tex.u2; + UVb.Y := Mix(gGFXData[rxGui, texID].Tex.v2, gGFXData[rxGui, texID].Tex.v1, heightInPx / gGFXData[rxGui, texID].pxHeight); + + y1 := pY - 1 - (gTerrain.LandExt^[pY, pX].RenderHeight + FO + VO) / CELL_HEIGHT_DIV; + y2 := pY - (gTerrain.LandExt^[pY + 1, pX].RenderHeight + VO) / CELL_HEIGHT_DIV; + + fenceX := gGFXData[rxGui,texID].PxWidth / CELL_SIZE_PX; + + case Pos of + dirW: x1 := pX - 1 - 3 / CELL_SIZE_PX; + dirE: x1 := pX - 1 + 3 / CELL_SIZE_PX - fenceX; + else x1 := pX - 1; //Should never happen + end; + + glBegin(GL_QUADS); + glTexCoord2f(UVa.x, UVa.y); glVertex2f(x1, y1); + glTexCoord2f(UVb.x, UVa.y); glVertex2f(x1+ fenceX, y1); + glTexCoord2f(UVb.x, UVb.y); glVertex2f(x1+ fenceX, y2); + glTexCoord2f(UVa.x, UVb.y); glVertex2f(x1, y2); + glEnd; + end; +end; + + +procedure TKMRenderTerrain.RenderMarkup(pX, pY: Word; aFieldType: TKMFieldType); +const + MarkupTex: array [TKMFieldType] of Word = (0, 105, 107, 108, 0); +var + ID: Integer; + UVa,UVb: TKMPointF; +begin + ID := MarkupTex[aFieldType]; + + TKMRender.BindTexture(gGFXData[rxGui, ID].Tex.TexID); + + UVa.X := gGFXData[rxGui, ID].Tex.u1; + UVa.Y := gGFXData[rxGui, ID].Tex.v1; + UVb.X := gGFXData[rxGui, ID].Tex.u2; + UVb.Y := gGFXData[rxGui, ID].Tex.v2; + + glBegin(GL_QUADS); + glTexCoord2f(UVb.x, UVa.y); glVertex2f(pX-1, pY-1 - gTerrain.LandExt^[pY ,pX ].RenderHeight/CELL_HEIGHT_DIV+0.10); + glTexCoord2f(UVa.x, UVa.y); glVertex2f(pX-1, pY-1 - gTerrain.LandExt^[pY ,pX ].RenderHeight/CELL_HEIGHT_DIV-0.15); + glTexCoord2f(UVa.x, UVb.y); glVertex2f(pX , pY - gTerrain.LandExt^[pY+1,pX+1].RenderHeight/CELL_HEIGHT_DIV-0.25); + glTexCoord2f(UVb.x, UVb.y); glVertex2f(pX , pY - gTerrain.LandExt^[pY+1,pX+1].RenderHeight/CELL_HEIGHT_DIV); + + glTexCoord2f(UVb.x, UVa.y); glVertex2f(pX-1, pY - gTerrain.LandExt^[pY+1,pX ].RenderHeight/CELL_HEIGHT_DIV); + glTexCoord2f(UVa.x, UVa.y); glVertex2f(pX-1, pY - gTerrain.LandExt^[pY+1,pX ].RenderHeight/CELL_HEIGHT_DIV-0.25); + glTexCoord2f(UVa.x, UVb.y); glVertex2f(pX , pY-1 - gTerrain.LandExt^[pY ,pX+1].RenderHeight/CELL_HEIGHT_DIV-0.15); + glTexCoord2f(UVb.x, UVb.y); glVertex2f(pX , pY-1 - gTerrain.LandExt^[pY ,pX+1].RenderHeight/CELL_HEIGHT_DIV+0.10); + glEnd; +end; + + +end.