Added auto cropping to atlasing
Added splitting of animation sheets to frames and auto crop the frames.
Fixed some atlas blitting issues. Vertex coords are still improper tho for
auto cropped frames
(*
* Hedgewars, a free turn based strategy game
* Copyright (c) 2004-2012 Andrey Korotaev <unC0Rr@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
*)
{$INCLUDE "options.inc"}
unit uTextures;
interface
uses SDLh, uTypes;
function NewTexture(width, height: Longword; buf: Pointer): PTexture;
procedure Surface2GrayScale(surf: PSDL_Surface);
function SurfaceSheet2Atlas(surf: PSDL_Surface; spriteWidth: Integer; spriteHeight: Integer): PTexture;
function Surface2Atlas(surf: PSDL_Surface; enableClamp: boolean): PTexture;
procedure FreeTexture(tex: PTexture);
procedure ComputeTexcoords(texture: PTexture; r: PSDL_Rect; tb: PVertexRect);
procedure initModule;
procedure freeModule;
implementation
uses GLunit, uUtils, uVariables, uConsts, uDebug, uConsole, uAtlas, SysUtils;
var
logFile: TextFile;
function CropSurface(source: PSDL_Surface; rect: PSDL_Rect): PSDL_Surface;
var
fmt: PSDL_PixelFormat;
srcP, dstP: PByte;
copySize: Integer;
i: Integer;
const
pixelSize = 4;
begin
//writeln(stdout, 'Cropping from ' + IntToStr(source^.w) + 'x' + IntToStr(source^.h) + ' -> ' + IntToStr(rect^.w) + 'x' + IntToStr(rect^.h));
fmt:= source^.format;
CropSurface:= SDL_CreateRGBSurface(source^.flags, rect^.w, rect^.h,
fmt^.BitsPerPixel, fmt^.Rmask, fmt^.Gmask, fmt^.Bmask, fmt^.Amask);
if SDL_MustLock(source) then
SDLTry(SDL_LockSurface(source) >= 0, true);
if SDL_MustLock(CropSurface) then
SDLTry(SDL_LockSurface(CropSurface) >= 0, true);
srcP:= source^.pixels;
dstP:= CropSurface^.pixels;
inc(srcP, pixelSize * rect^.x);
inc(srcP, source^.pitch * rect^.y);
copySize:= rect^.w * pixelSize;
for i:= 0 to Pred(rect^.h) do
begin
Move(srcP^, dstP^, copySize);
inc(srcP, source^.pitch);
inc(dstP, CropSurface^.pitch);
end;
if SDL_MustLock(source) then
SDL_UnlockSurface(source);
if SDL_MustLock(CropSurface) then
SDL_UnlockSurface(CropSurface);
end;
function TransparentLine(p: PByte; stride: Integer; length: Integer): boolean;
var
i: Integer;
begin
TransparentLine:= false;
for i:=0 to pred(length) do
begin
if p^ <> 0 then
exit;
inc(p, stride);
end;
TransparentLine:= true;
end;
function AutoCrop(source: PSDL_Surface; var cropinfo: TCropInformation): PSDL_Surface;
var
l,r,t,b, i: Integer;
pixels, p: PByte;
scanlineSize: Integer;
rect: TSDL_Rect;
const
pixelSize = 4;
begin
l:= source^.w;
r:= 0;
t:= source^.h;
b:= 0;
if SDL_MustLock(source) then
SDLTry(SDL_LockSurface(source) >= 0, true);
pixels:= source^.pixels;
scanlineSize:= source^.pitch;
inc(pixels, 3); // advance to alpha value
// check top
p:= pixels;
for i:= 0 to Pred(source^.h) do
begin
if not TransparentLine(p, pixelSize, source^.w) then
begin
t:= i;
break;
end;
inc(p, scanlineSize);
end;
// check bottom
p:= pixels;
inc(p, scanlineSize * source^.h);
for i:= 0 to Pred(source^.h - t) do
begin
dec(p, scanlineSize);
if not TransparentLine(p, pixelSize, source^.w) then
begin
b:= i;
break;
end;
end;
// check left
p:= pixels;
for i:= 0 to Pred(source^.w) do
begin
if not TransparentLine(p, scanlineSize, source^.h) then
begin
l:= i;
break;
end;
inc(p, pixelSize);
end;
// check right
p:= pixels;
inc(p, scanlineSize);
for i:= 0 to Pred(source^.w - l) do
begin
dec(p, pixelSize);
if not TransparentLine(p, scanlineSize, source^.h) then
begin
r:= i;
break;
end;
end;
if SDL_MustLock(source) then
SDL_UnlockSurface(source);
rect.x:= l;
rect.y:= t;
rect.w:= source^.w - r - l;
rect.h:= source^.h - b - t;
cropInfo.l:= l;
cropInfo.r:= r;
cropInfo.t:= t;
cropInfo.b:= b;
cropInfo.x:= Trunc(source^.w / 2 - l + r);
cropInfo.y:= Trunc(source^.h / 2 - t + b);
if (l = source^.w) or (t = source^.h) then
begin
result:= nil;
exit;
end;
if (l <> 0) or (r <> 0) or (t <> 0) or (b <> 0) then
result:= CropSurface(source, @rect)
else result:= source;
end;
procedure SetTextureParameters(enableClamp: Boolean);
begin
if enableClamp and ((cReducedQuality and rqClampLess) = 0) then
begin
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
end;
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
end;
procedure ComputeTexcoords(texture: PTexture; r: PSDL_Rect; tb: PVertexRect);
var
x0, y0, x1, y1, tmp: Real;
w, h, aw, ah: LongInt;
p: PChar;
const
texelOffset = 0.0;
begin
aw:=texture^.atlas^.w;
ah:=texture^.atlas^.h;
if texture^.isRotated then
begin
w:=r^.h;
h:=r^.w;
end else
begin
w:=r^.w;
h:=r^.h;
end;
x0:= (texture^.x + {r^.x} + texelOffset)/aw;
x1:= (texture^.x + {r^.x} + w - texelOffset)/aw;
y0:= (texture^.y + {r^.y} + texelOffset)/ah;
y1:= (texture^.y + {r^.y} + h - texelOffset)/ah;
if (texture^.isRotated) then
begin
tb^[0].X:= x0;
tb^[0].Y:= y0;
tb^[3].X:= x1;
tb^[3].Y:= y0;
tb^[2].X:= x1;
tb^[2].Y:= y1;
tb^[1].X:= x0;
tb^[1].Y:= y1
end else
begin
tb^[0].X:= x0;
tb^[0].Y:= y0;
tb^[1].X:= x1;
tb^[1].Y:= y0;
tb^[2].X:= x1;
tb^[2].Y:= y1;
tb^[3].X:= x0;
tb^[3].Y:= y1;
end;
end;
procedure ResetVertexArrays(texture: PTexture);
var r: TSDL_Rect;
begin
with texture^ do
begin
vb[0].X:= texture^.cropInfo.l;
vb[0].Y:= texture^.cropInfo.t;
vb[1].X:= texture^.cropInfo.l + w;
vb[1].Y:= texture^.cropInfo.t;
vb[2].X:= texture^.cropInfo.l + w;
vb[2].Y:= texture^.cropInfo.t + h;
vb[3].X:= texture^.cropInfo.l;
vb[3].Y:= texture^.cropInfo.t + h;
end;
r.x:= 0;
r.y:= 0;
r.w:= texture^.w;
r.h:= texture^.h;
ComputeTexcoords(texture, @r, @texture^.tb);
end;
function NewTexture(width, height: Longword; buf: Pointer): PTexture;
begin
new(NewTexture);
NewTexture^.Scale:= 1;
// Atlas allocation happens here later on. For now we just allocate one exclusive atlas per sprite
new(NewTexture^.atlas);
NewTexture^.atlas^.w:=width;
NewTexture^.atlas^.h:=height;
NewTexture^.x:=0;
NewTexture^.y:=0;
NewTexture^.w:=width;
NewTexture^.h:=height;
NewTexture^.isRotated:=false;
NewTexture^.shared:=false;
NewTexture^.surface:=nil;
NewTexture^.nextFrame:=nil;
NewTexture^.cropInfo.l:= 0;
NewTexture^.cropInfo.r:= 0;
NewTexture^.cropInfo.t:= 0;
NewTexture^.cropInfo.b:= 0;
NewTexture^.cropInfo.x:= width div 2;
NewTexture^.cropInfo.y:= height div 2;
ResetVertexArrays(NewTexture);
glGenTextures(1, @NewTexture^.atlas^.id);
glBindTexture(GL_TEXTURE_2D, NewTexture^.atlas^.id);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, buf);
SetTextureParameters(true);
end;
procedure Surface2GrayScale(surf: PSDL_Surface);
var tw, x, y: Longword;
fromP4: PLongWordArray;
begin
fromP4:= Surf^.pixels;
for y:= 0 to Pred(Surf^.h) do
begin
for x:= 0 to Pred(Surf^.w) do
begin
tw:= fromP4^[x];
tw:= round((tw shr RShift and $FF) * RGB_LUMINANCE_RED +
(tw shr GShift and $FF) * RGB_LUMINANCE_GREEN +
(tw shr BShift and $FF) * RGB_LUMINANCE_BLUE);
if tw > 255 then tw:= 255;
tw:= (tw and $FF shl RShift) or (tw and $FF shl BShift) or (tw and $FF shl GShift) or (fromP4^[x] and AMask);
fromP4^[x]:= tw;
end;
fromP4:= @(fromP4^[Surf^.pitch div 4])
end;
end;
function SurfaceSheet2Atlas(surf: PSDL_Surface; spriteWidth: Integer; spriteHeight: Integer): PTexture;
var
subSurface: PSDL_Surface;
framesX, framesY: Integer;
last, current: PTexture;
r: TSDL_Rect;
x, y: Integer;
begin
SurfaceSheet2Atlas:= nil;
r.x:= 0;
r.y:= 0;
r.w:= spriteWidth;
r.h:= spriteHeight;
last:= nil;
framesX:= surf^.w div spriteWidth;
framesY:= surf^.h div spriteHeight;
for x:=0 to Pred(framesX) do
begin
r.y:= 0;
for y:=0 to Pred(framesY) do
begin
subSurface:= CropSurface(surf, @r);
current:= Surface2Atlas(subSurface, false);
if last = nil then
begin
SurfaceSheet2Atlas:= current;
last:= current;
end else
begin
last^.nextFrame:= current;
last:= current;
end;
inc(r.y, spriteHeight);
end;
inc(r.x, spriteWidth);
end;
SDL_FreeSurface(surf);
end;
function Surface2Atlas(surf: PSDL_Surface; enableClamp: boolean): PTexture;
var tw, th, x, y: Longword;
tmpp: pointer;
cropped: PSDL_Surface;
fromP4, toP4: PLongWordArray;
cropInfo: TCropInformation;
begin
cropped:= AutoCrop(surf, cropInfo);
if cropped <> surf then
begin
SDL_FreeSurface(surf);
surf:= cropped;
end;
if surf = nil then
begin
new(Surface2Atlas);
Surface2Atlas^.w:= 0;
Surface2Atlas^.h:= 0;
Surface2Atlas^.x:=0 ;
Surface2Atlas^.y:=0 ;
Surface2Atlas^.isRotated:= false;
Surface2Atlas^.surface:= nil;
Surface2Atlas^.shared:= false;
Surface2Atlas^.nextFrame:= nil;
Surface2Atlas^.cropInfo:= cropInfo;
exit;
end;
if (surf^.w <= 512) and (surf^.h <= 512) then
begin
Surface2Atlas:= Surface2Tex_(surf, enableClamp); // run the atlas side by side for debugging
Surface2Atlas^.cropInfo:= cropInfo;
ResetVertexArrays(Surface2Atlas);
exit;
end;
new(Surface2Atlas);
// Atlas allocation happens here later on. For now we just allocate one exclusive atlas per sprite
new(Surface2Atlas^.atlas);
Surface2Atlas^.w:= surf^.w;
Surface2Atlas^.h:= surf^.h;
Surface2Atlas^.x:=0;
Surface2Atlas^.y:=0;
Surface2Atlas^.isRotated:=false;
Surface2Atlas^.surface:= surf;
Surface2Atlas^.shared:= false;
Surface2Atlas^.nextFrame:= nil;
Surface2Atlas^.cropInfo:= cropInfo;
if (surf^.format^.BytesPerPixel <> 4) then
begin
TryDo(false, 'Surface2Tex failed, expecting 32 bit surface', true);
Surface2Atlas^.atlas^.id:= 0;
exit;
end;
glGenTextures(1, @Surface2Atlas^.atlas^.id);
glBindTexture(GL_TEXTURE_2D, Surface2Atlas^.atlas^.id);
if SDL_MustLock(surf) then
SDLTry(SDL_LockSurface(surf) >= 0, true);
fromP4:= Surf^.pixels;
if GrayScale then
Surface2GrayScale(Surf);
if (not SupportNPOTT) and (not (isPowerOf2(Surf^.w) and isPowerOf2(Surf^.h))) then
begin
tw:= toPowerOf2(Surf^.w);
th:= toPowerOf2(Surf^.h);
Surface2Atlas^.atlas^.w:=tw;
Surface2Atlas^.atlas^.h:=th;
tmpp:= GetMem(tw * th * surf^.format^.BytesPerPixel);
fromP4:= Surf^.pixels;
toP4:= tmpp;
for y:= 0 to Pred(Surf^.h) do
begin
for x:= 0 to Pred(Surf^.w) do
toP4^[x]:= fromP4^[x];
for x:= Surf^.w to Pred(tw) do
toP4^[x]:= 0;
toP4:= @(toP4^[tw]);
fromP4:= @(fromP4^[Surf^.pitch div 4])
end;
for y:= Surf^.h to Pred(th) do
begin
for x:= 0 to Pred(tw) do
toP4^[x]:= 0;
toP4:= @(toP4^[tw])
end;
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, tw, th, 0, GL_RGBA, GL_UNSIGNED_BYTE, tmpp);
FreeMem(tmpp, tw * th * surf^.format^.BytesPerPixel)
end
else
begin
Surface2Atlas^.atlas^.w:=Surf^.w;
Surface2Atlas^.atlas^.h:=Surf^.h;
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, surf^.w, surf^.h, 0, GL_RGBA, GL_UNSIGNED_BYTE, surf^.pixels);
end;
ResetVertexArrays(Surface2Atlas);
if SDL_MustLock(surf) then
SDL_UnlockSurface(surf);
SetTextureParameters(enableClamp);
end;
// deletes texture and frees the memory allocated for it.
// if nil is passed nothing is done
procedure FreeTexture(tex: PTexture);
begin
if tex <> nil then
begin
FreeTexture(tex^.nextFrame); // free all frames linked to this animation
if tex^.surface = nil then
begin
Dispose(tex);
exit;
end;
if tex^.shared then
begin
SDL_FreeSurface(tex^.surface);
FreeTexture_(tex); // run atlas side by side for debugging
exit;
end;
// Atlas cleanup happens here later on. For now we just free as each sprite has one atlas
glDeleteTextures(1, @tex^.atlas^.id);
Dispose(tex^.atlas);
if (tex^.surface <> nil) then
SDL_FreeSurface(tex^.surface);
Dispose(tex);
end
end;
procedure initModule;
begin
assign(logFile, 'out.log');
rewrite(logFile);
uAtlas.initModule;
end;
procedure freeModule;
begin
close(logFile);
end;
end.