unit KM_Render; {$I KaM_Remake.inc} interface uses {$IFDEF WDC} Windows, {$ENDIF} {$IFDEF Unix} LCLIntf, LCLType, OpenGLContext, {$ENDIF} dglOpenGL, KM_RenderTypes, KM_CommonTypes; const TEX_FORMAT_SIZE: array [TKMTexFormat] of Byte = (2, 4, 1); type TKMRenderMode = (rm2D); TKMSampledVertex = record X, Y, Z: Single; U, V: Single; end; //General OpenGL handling TKMRender = class private class var fMaxViewportDim: Cardinal; // Max Viewport Dimensions, which could be used for FBO render private // Flag, True when FBO is inited. We don't need to init FBO for every run, since its only used in SaveMapToImageFile feature fFBOInited: Boolean; // Off-screen buffer, example from https://stackoverflow.com/questions/12157646/how-to-render-offscreen-on-opengl fFBO: Cardinal; fRenderBuf: Cardinal; fScreenX, fScreenY: Word; procedure InitFBO; procedure SaveBufferToFile(aUseFBOBuffer: Boolean; var aWidth, aHeight: Integer; var aPixelData: TKMCardinalArray); function GetMaxFBOSize: Cardinal; public constructor Create(aScreenX, aScreenY: Integer); destructor Destroy; override; 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; class function GenTexture(DestX, DestY: Word; const Data: Pointer; Mode: TKMTexFormat; aMinFilter, aMagFilter: TKMFilterType): Cardinal; class procedure DeleteTexture(aTex: Cardinal); class procedure UpdateTexture(aTexture: Cardinal; DestX, DestY: Word; Mode: TKMTexFormat; const Data: Pointer); class procedure BindTexture(aTexId: Cardinal); class procedure InvalidateTextureBindingCache; static; class property MaxTextureSize: Cardinal read GetMaxTexSize; class property MaxViewportDim: Cardinal read GetMaxViewportDim; property MaxFBOSize: Cardinal read GetMaxFBOSize; procedure Resize(aWidth, aHeight: Integer); procedure ReadRenderedToScreenPixels(var aWidth, aHeight: Integer; var aPixelData: TKMCardinalArray); procedure ReadRenderedToFBOPixels(var aWidth, aHeight: Integer; var aPixelData: TKMCardinalArray); property ScreenX: Word read fScreenX; property ScreenY: Word read fScreenY; procedure BeginFrame; procedure RenderBrightness(aValue: Byte); end; var gRender: TKMRender; implementation uses SysUtils, Math, CastleRenderContext, CastleRenderOptions, KromOGLUtils, KromUtils, KM_Defaults, KM_IoPNG, KM_CommonClasses, KM_Render_Mobile; const // We want to use off-screen FBO buffer to render full map to the file, // we are restricted with max file we are able to write via TBitmap or TJpegImage or TPNGImage atm // could check https://github.com/graphics32 package to solve this issue with saving ultra large images MAX_FBO_BUFFER_SIZE = CELL_SIZE_PX * (MAX_MAP_SIZE - 1); // (10200 = CELL_SIZE=40px)*255 { TKMRender } constructor TKMRender.Create(aScreenX, aScreenY: Integer); begin inherited Create; fFBOInited := False; if not fBlind then begin fMaxViewportDim := GetMaxViewportDim; //glEnable(GL_CULL_FACE); //glCullFace(GL_FRONT); end; Resize(aScreenX, aScreenY); end; destructor TKMRender.Destroy; begin KMRenderMobileInvalidateSampledProgram; if not fBlind then begin if fFBOInited then begin glDeleteFramebuffers(1,@fFBO); glDeleteRenderbuffers(1,@fRenderBuf); end; end; inherited; end; procedure TKMRender.InitFBO; begin if fFBOInited then Exit; fFBOInited := True; glGenFramebuffers(1, @fFBO); glGenRenderbuffers(1, @fRenderBuf); glBindRenderbuffer(GL_RENDERBUFFER, fRenderBuf); glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA, GetMaxFBOSize, GetMaxFBOSize); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fFBO); glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, fRenderBuf); end; //We have to use this method EVERY time we want to bind texture. Otherwise collisions could happen // //Do not bind same texture again, it can drastically change render performance //F.e. on an average Map (Cube 256x256) when full map is shown in viewport //there are only ~10k new texture binds, when all other ~30k binds can be skipped class procedure TKMRender.BindTexture(aTexId: Cardinal); begin KMRenderMobileBindTexture(aTexId); end; class procedure TKMRender.InvalidateTextureBindingCache; begin KMRenderMobileInvalidateTextureBindingCache; end; procedure TKMRender.Resize(aWidth, aHeight: Integer); begin fScreenX := max(aWidth, 1); fScreenY := max(aHeight, 1); end; procedure TKMRender.SetRenderMode(aRenderMode: TKMRenderMode); begin //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 KMRenderMobileApplyOrdinary2DProjection(fScreenX, fScreenY); end; class function TKMRender.GenerateTextureCommon(aMinFilter, aMagFilter: TKMFilterType): Cardinal; begin Result := KMRenderMobileCreateTexture(aMinFilter, aMagFilter); end; //Generate texture out of TCardinalArray class function TKMRender.GenTexture(DestX, DestY: Word; const Data: Pointer; Mode: TKMTexFormat; aMinFilter, aMagFilter: TKMFilterType): Cardinal; var texture: Cardinal; begin texture := GenerateTextureCommon(aMinFilter, aMagFilter); try UpdateTexture(texture, DestX, DestY, Mode, Data); Result := texture; except DeleteTexture(texture); raise; end; end; class procedure TKMRender.DeleteTexture(aTex: Cardinal); begin KMRenderMobileDeleteTexture(aTex); end; function TKMRender.GetMaxFBOSize: Cardinal; begin Result := Min(fMaxViewportDim, MAX_FBO_BUFFER_SIZE); end; class procedure TKMRender.GraphicsContextChanged(aAvailable: Boolean); begin if aAvailable then begin KMRenderMobileInitializeCurrentContext; KMRenderMobileInitializeSampledProgram; end else begin KMRenderMobileInvalidateCurrentContext; KMRenderMobileInvalidateSampledProgram; end; end; class function TKMRender.TextureBackendReady: Boolean; begin Result := KMRenderMobileTextureBackendReady; 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; end; class function TKMRender.GetMaxViewportDim: Cardinal; begin if fMaxViewportDim > 0 then Exit(fMaxViewportDim); //Get max supported dimensions for viewport (used in off-screen render, FBO) glGetIntegerv(GL_MAX_VIEWPORT_DIMS, @fMaxViewportDim); Result := fMaxViewportDim; end; //Update texture with TCardinalArray class procedure TKMRender.UpdateTexture(aTexture: Cardinal; DestX, DestY: Word; Mode: TKMTexFormat; const Data: Pointer); begin KMRenderMobileUpdateTexture(aTexture, DestX, DestY, Mode, Data); end; procedure TKMRender.ReadRenderedToScreenPixels(var aWidth, aHeight: Integer; var aPixelData: TKMCardinalArray); begin SaveBufferToFile(False, aWidth, aHeight, aPixelData); end; procedure TKMRender.ReadRenderedToFBOPixels(var aWidth, aHeight: Integer; var aPixelData: TKMCardinalArray); begin SaveBufferToFile(True, aWidth, aHeight, aPixelData); end; procedure TKMRender.SaveBufferToFile(aUseFBOBuffer: Boolean; var aWidth, aHeight: Integer; var aPixelData: TKMCardinalArray); {$IFDEF WDC} var I, K: Integer; {$ENDIF} begin {$IFDEF WDC} aWidth := ScreenX; aHeight := ScreenY; if aUseFBOBuffer then begin // Read from offscreen buffer glReadBuffer(GL_COLOR_ATTACHMENT0); // Bind to FBO framebuffer glBindFramebuffer(GL_READ_FRAMEBUFFER, fFBO); end else // Read from onscreen buffer glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); SetLength(aPixelData, aWidth * aHeight + 1); glReadPixels(0, 0, aWidth, aHeight, GL_BGRA, GL_UNSIGNED_BYTE, @aPixelData[0]); if aUseFBOBuffer then // Return to onscreen rendering: glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); //Mirror verticaly for i := 0 to (aHeight div 2) - 1 do for K := 0 to aWidth - 1 do SwapInt(aPixelData[i * aWidth + K], aPixelData[((aHeight - 1) - i) * aWidth + K]); {$ENDIF} end; procedure TKMRender.BeginFrame; begin if SAVE_MAP_TO_FBO_RENDER then begin if not fFBOInited then // Init once InitFBO // glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fFBO) is called while initialization else glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fFBO); end; 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 //There will be no change to image anyway if aValue = 0 then Exit; intensity := aValue / 20; KMRenderMobileDrawBrightnessOverlay(intensity, ScreenX, ScreenY); end; end.