Files
kam-mobile/port-source/kam_remake/src/render/KM_Render.pas
T

355 lines
9.9 KiB
ObjectPascal
Raw Normal View History

2026-10-04 17:46:01 +05:00
unit KM_Render;
{$I KaM_Remake.inc}
interface
uses
{$IFDEF WDC} Windows, {$ENDIF}
{$IFDEF Unix} LCLIntf, LCLType, OpenGLContext, {$ENDIF}
dglOpenGL,
2026-10-06 12:49:37 +05:00
KM_RenderTypes, KM_CommonTypes;
2026-10-04 17:46:01 +05:00
const
TEX_FORMAT_SIZE: array [TKMTexFormat] of Byte = (2, 4, 1);
type
2026-10-08 11:59:40 +05:00
TKMRenderMode = (rm2D);
TKMSampledVertex = record
X, Y, Z: Single;
U, V: Single;
end;
2026-10-04 17:46:01 +05:00
//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
2026-10-06 12:08:28 +05:00
constructor Create(aScreenX, aScreenY: Integer);
2026-10-04 17:46:01 +05:00
destructor Destroy; override;
2026-10-08 11:59:40 +05:00
procedure SetRenderMode(aRenderMode: TKMRenderMode);
2026-10-04 17:46:01 +05:00
class procedure GraphicsContextChanged(aAvailable: Boolean); static;
class function TextureBackendReady: Boolean; static;
2026-10-08 11:59:40 +05:00
class function SampledProgramReady: Boolean; static;
class procedure DrawSampledTriangles(const aVertices: array of TKMSampledVertex; const aColor: TKMColor4f); static;
2026-10-04 17:46:01 +05:00
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,
2026-10-08 11:59:40 +05:00
CastleRenderContext, CastleRenderOptions,
2026-10-04 17:46:01 +05:00
KromOGLUtils, KromUtils,
2026-10-08 11:59:40 +05:00
KM_Defaults, KM_IoPNG, KM_CommonClasses,
2026-10-04 17:46:01 +05:00
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 }
2026-10-06 12:08:28 +05:00
constructor TKMRender.Create(aScreenX, aScreenY: Integer);
2026-10-04 17:46:01 +05:00
begin
inherited Create;
fFBOInited := False;
if not fBlind then
begin
fMaxViewportDim := GetMaxViewportDim;
//glEnable(GL_CULL_FACE);
//glCullFace(GL_FRONT);
end;
2026-10-06 12:08:28 +05:00
Resize(aScreenX, aScreenY);
2026-10-04 17:46:01 +05:00
end;
destructor TKMRender.Destroy;
begin
2026-10-08 11:59:40 +05:00
KMRenderMobileInvalidateSampledProgram;
2026-10-04 17:46:01 +05:00
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
2026-10-08 11:59:40 +05:00
KMRenderMobileApplyOrdinary2DProjection(fScreenX, fScreenY);
2026-10-04 17:46:01 +05:00
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
2026-10-08 11:59:40 +05:00
begin
KMRenderMobileInitializeCurrentContext;
KMRenderMobileInitializeSampledProgram;
end
2026-10-04 17:46:01 +05:00
else
2026-10-08 11:59:40 +05:00
begin
2026-10-04 17:46:01 +05:00
KMRenderMobileInvalidateCurrentContext;
2026-10-08 11:59:40 +05:00
KMRenderMobileInvalidateSampledProgram;
end;
2026-10-04 17:46:01 +05:00
end;
class function TKMRender.TextureBackendReady: Boolean;
begin
Result := KMRenderMobileTextureBackendReady;
end;
2026-10-08 11:59:40 +05:00
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;
2026-10-04 17:46:01 +05:00
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;
2026-10-08 11:59:40 +05:00
KMRenderMobileClearCurrentTarget;
2026-10-04 17:46:01 +05:00
SetRenderMode(rm2D);
2026-10-08 11:59:40 +05:00
KMRenderMobileSetActiveTextureUnit0;
TKMRender.InvalidateTextureBindingCache;
RenderContext.DepthFunc := dfLessEqual;
RenderContext.BlendingEnable(bsSrcAlpha, bdOneMinusSrcAlpha);
2026-10-04 17:46:01 +05:00
//RC.Activate for OSX
end;
//Render highlight overlay to make whole picture look brighter (more saturated)
procedure TKMRender.RenderBrightness(aValue: Byte);
2026-10-08 11:59:40 +05:00
var
intensity: Single;
2026-10-04 17:46:01 +05:00
begin
//There will be no change to image anyway
if aValue = 0 then Exit;
2026-10-08 11:59:40 +05:00
intensity := aValue / 20;
KMRenderMobileDrawBrightnessOverlay(intensity, ScreenX, ScreenY);
2026-10-04 17:46:01 +05:00
end;
end.