hedgewars/uTextures.pas
author Wolfgang Steffens <WolfgangSteff@gmail.com>
Tue, 10 Jul 2012 16:38:50 +0200
changeset 7377 1aceade403ba
parent 7304 8b3575750cd2
child 11317 62287d4044e7
permissions -rw-r--r--
Debug commit Added code to replace sprites with a checkerboard pattern in order to identify filtering issues. This version only renders DrawSprite()'s with the checkerboard pattern for debugging purposes cleaned up some unneeded stuff

(*
 * 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 
    texelOffsetPos = 0.5;
    texelOffsetNeg = 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} +     texelOffsetPos)/aw;
    x1:= (texture^.x + {r^.x} + w + texelOffsetNeg)/aw;
    y0:= (texture^.y + {r^.y} +     texelOffsetPos)/ah;
    y1:= (texture^.y + {r^.y} + h + texelOffsetNeg)/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 
    rect: TSDL_Rect;
    l, t, r, b: Real;
const
    halfTexelOffsetPos = 1.0;
    halfTexelOffsetNeg = -0.0;
begin
    l:= texture^.cropInfo.l + halfTexelOffsetPos;
    r:= texture^.cropInfo.l + texture^.w + halfTexelOffsetNeg;
    t:= texture^.cropInfo.t + halfTexelOffsetPos;
    b:= texture^.cropInfo.t + texture^.h + halfTexelOffsetNeg;

    with texture^ do
    begin
        vb[0].X:= l;
        vb[0].Y:= t;
        vb[1].X:= r;
        vb[1].Y:= t;
        vb[2].X:= r;
        vb[2].Y:= b;
        vb[3].X:= l;
        vb[3].Y:= b;
    end;

    rect.x:= 0;
    rect.y:= 0;
    rect.w:= texture^.w;
    rect.h:= texture^.h;
    ComputeTexcoords(texture, @rect, @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
    // nothing should use the old codepath anymore once we are done!
    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.