mirror of
https://github.com/dpethes/rerogue.git
synced 2025-06-07 18:58:32 +02:00
335 lines
10 KiB
ObjectPascal
335 lines
10 KiB
ObjectPascal
unit terrain_mesh;
|
|
{$mode objfpc}{$H+}
|
|
|
|
interface
|
|
|
|
uses
|
|
Classes, SysUtils, matrix,
|
|
gl, glext, glu,
|
|
rs_world, vector_util;
|
|
|
|
type
|
|
TRenderOpts = record
|
|
wireframe: boolean;
|
|
points: boolean;
|
|
vcolors: boolean;
|
|
textures: boolean;
|
|
fg_to_draw: integer;
|
|
end;
|
|
|
|
TTerrainBlock = record
|
|
texture_index: integer;
|
|
vertices: array[0..24] of Tvector3_single; //25*3*4 = 300B
|
|
normals: array[0..24] of Tvector3_single; //25*3*4 = 300B
|
|
end; //~600B per block
|
|
PTerrainBlock = ^TTerrainBlock;
|
|
|
|
TRenderBlockBatch = record
|
|
texture_gl_index: integer;
|
|
blocks: integer;
|
|
vertices: PSingle;
|
|
normals: PSingle;
|
|
face_indices: PInteger;
|
|
end;
|
|
|
|
{ TTerrainMesh }
|
|
TTerrainMesh = class
|
|
const
|
|
FacesPerBlock = 4 * 4 * 2;
|
|
private
|
|
terrain: TWorld;
|
|
blocks: array of array of TTerrainBlock;
|
|
block_texcoords: array of Tvector2_single; //static, 25*2*4 = 200B
|
|
block_face_indices: array[0..FacesPerBlock*3 - 1] of byte; //static, 96B
|
|
render_batches: array of TRenderBlockBatch;
|
|
|
|
function TileToBlock(var tile: TTile; basey, basex: integer): TTerrainBlock;
|
|
procedure TransformTiles;
|
|
procedure InitBlockStaticData;
|
|
public
|
|
destructor Destroy; override;
|
|
procedure Load(level: TLevelListItem);
|
|
procedure InitGL;
|
|
procedure DrawGL(opts: TRenderOpts);
|
|
end;
|
|
|
|
implementation
|
|
|
|
const
|
|
VerticesPerBlock = 5 * 5;
|
|
|
|
function TerrainBlockOrderByTex(Item1: Pointer; Item2: Pointer): integer;
|
|
var
|
|
a, b: PTerrainBlock;
|
|
begin
|
|
a := PTerrainBlock(Item1);
|
|
b := PTerrainBlock(Item2);
|
|
result := a^.texture_index - b^.texture_index;
|
|
end;
|
|
|
|
{ TileToBlock
|
|
Create single terrain block at given position using RS3D tile.
|
|
Basex/y is the position in block units for given dimension.
|
|
While vertical scaling is stored within file, horizontal seems to be always 0.5.
|
|
The generated x, y coords are flipped for (opengl) rendering.
|
|
}
|
|
function TTerrainMesh.TileToBlock(var tile: TTile; basey, basex: integer): TTerrainBlock;
|
|
const
|
|
h_scale = 0.5;
|
|
var
|
|
x, y, idx: integer;
|
|
vx, vy, vz: single;
|
|
v_scale: single;
|
|
begin
|
|
result.texture_index := tile.texture_index;
|
|
//dim * vertices_per_tile - half_tile_dim * vertices_per_tile
|
|
basey := basey * 4 - terrain.TileHeight * 2;
|
|
basex := basex * 4 - terrain.TileWidth * 2;
|
|
v_scale := terrain.heightmap.y_scale;
|
|
for y := 0 to 4 do
|
|
for x := 0 to 4 do begin
|
|
idx := y * 5 + x;
|
|
//x,y,z, rotated by 180 around z
|
|
vx := (basex + x) * h_scale * -1;
|
|
vy := tile.heights[idx] * v_scale * -1;
|
|
vz := (basey + y) * h_scale;
|
|
result.vertices[idx].init(vx, vy, vz);
|
|
end;
|
|
end;
|
|
|
|
{ TransformTiles
|
|
Create terrain blocks from RS3D data
|
|
}
|
|
procedure TTerrainMesh.TransformTiles;
|
|
|
|
//todo do proper per-vertex normal:
|
|
//this only calculates face normals and sets them to face's vertices, overwriting the value set
|
|
//from previous face
|
|
procedure FakeNormals(var blk: TTerrainBlock);
|
|
procedure SetTriData(const i0, i1, i2: byte);
|
|
var
|
|
normal: Tvector3_single;
|
|
begin
|
|
normal := GetNormal(blk.vertices[i0], blk.vertices[i1], blk.vertices[i2]);
|
|
blk.normals[i0] := normal;
|
|
blk.normals[i1] := normal;
|
|
blk.normals[i2] := normal;
|
|
end;
|
|
const
|
|
VertexStride = 5;
|
|
var
|
|
x, y, i: integer;
|
|
begin
|
|
for y := 0 to 3 do
|
|
for x := 0 to 3 do begin
|
|
i := y * VertexStride + x;
|
|
SetTriData(i, i+1, i+VertexStride);
|
|
SetTriData(i+1, i+VertexStride+1, i+VertexStride);
|
|
end;
|
|
end;
|
|
|
|
var
|
|
x, y, tile_idx: integer;
|
|
blk: TTerrainBlock;
|
|
tile: TTile;
|
|
begin
|
|
SetLength(blocks, terrain.TileHeight, terrain.TileWidth);
|
|
for y := 0 to terrain.TileHeight - 1 do begin
|
|
for x := 0 to terrain.TileWidth - 1 do begin
|
|
tile_idx := terrain.heightmap.blk[y * terrain.TileWidth + x];
|
|
tile := terrain.heightmap.tiles[tile_idx];
|
|
|
|
blk := TileToBlock(tile, y, x);
|
|
FakeNormals(blk);
|
|
blocks[y, x] := blk;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
{ InitBlockStaticData
|
|
Initializes data shared between tiles: face vertex indices, texture coords
|
|
}
|
|
procedure TTerrainMesh.InitBlockStaticData;
|
|
|
|
procedure SetTriData(const tri_idx: integer; const i0, i1, i2: byte);
|
|
begin
|
|
block_face_indices[tri_idx * 3 + 0] := i0;
|
|
block_face_indices[tri_idx * 3 + 1] := i1;
|
|
block_face_indices[tri_idx * 3 + 2] := i2;
|
|
end;
|
|
|
|
const
|
|
VertexStride = 5;
|
|
var
|
|
x, y, i, tri_idx: integer;
|
|
blk: integer;
|
|
begin
|
|
tri_idx := 0;
|
|
//init face indices
|
|
for y := 0 to 3 do
|
|
for x := 0 to 3 do begin
|
|
i := y * VertexStride + x;
|
|
SetTriData(tri_idx, i, i+1, i+VertexStride);
|
|
SetTriData(tri_idx+1, i+1, i+VertexStride+1, i+VertexStride);
|
|
tri_idx += 2;
|
|
end;
|
|
//init uv coords. block_texcoords is quite pessimistic
|
|
SetLength(block_texcoords, VerticesPerBlock * terrain.TileHeight * terrain.TileWidth);
|
|
for blk := 0 to Length(block_texcoords) div VerticesPerBlock - 1 do
|
|
for y := 0 to 4 do
|
|
for x := 0 to 4 do begin
|
|
block_texcoords[blk*25 + y * 5 + x].init(x/4, 1 - y/4); //u, v
|
|
end;
|
|
end;
|
|
|
|
destructor TTerrainMesh.Destroy;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
inherited Destroy;
|
|
for i := 0 to Length(render_batches) - 1 do begin
|
|
freemem(render_batches[i].vertices);
|
|
freemem(render_batches[i].normals);
|
|
freemem(render_batches[i].face_indices);
|
|
end;
|
|
render_batches := nil;
|
|
terrain.Free;
|
|
end;
|
|
|
|
procedure TTerrainMesh.Load(level: TLevelListItem);
|
|
begin
|
|
terrain := TWorld.Create;
|
|
terrain.LoadFromNodes(level);
|
|
TransformTiles;
|
|
InitBlockStaticData;
|
|
WriteLn(Format('terrain size: %dx%d, tris: %d',
|
|
[terrain.TileWidth, terrain.TileHeight,
|
|
terrain.TileWidth * terrain.TileHeight * 4 * 4 * 2]));
|
|
end;
|
|
|
|
{ InitGL
|
|
Prepare data for rendering: generate textures, sort blocks by texture and merge them to batches.
|
|
}
|
|
procedure TTerrainMesh.InitGL;
|
|
|
|
function GenTexture(tex_idx: integer; use_mip: boolean): integer;
|
|
const
|
|
TexW = 64;
|
|
TexH = 64;
|
|
var
|
|
texture_glidx: integer;
|
|
tex: pbyte;
|
|
begin
|
|
tex := terrain.heightmap.textures[tex_idx];
|
|
//pnm_save('tex'+IntToStr(tex_idx) + '.pnm', tex, TexW, TexH); //debug
|
|
glGenTextures(1, @texture_glidx);
|
|
glBindTexture(GL_TEXTURE_2D, texture_glidx);
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, TexW, TexH, 0, GL_RGB, GL_UNSIGNED_BYTE, tex);
|
|
if use_mip then begin
|
|
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR );
|
|
gluBuild2DMipmaps(GL_TEXTURE_2D, GL_RGB8, TexW, TexH, GL_RGB, GL_UNSIGNED_BYTE, tex);
|
|
end else begin
|
|
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
|
|
end;
|
|
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
|
|
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
result := texture_glidx;
|
|
end;
|
|
|
|
var
|
|
x, y: integer;
|
|
tex, k, vidx: integer;
|
|
render_blocks: TList;
|
|
start_idx, end_idx, block_count: integer;
|
|
element_array_size: integer;
|
|
begin
|
|
glEnable(GL_TEXTURE_2D);
|
|
|
|
render_blocks := TList.Create;
|
|
for y := 0 to terrain.TileHeight - 1 do
|
|
for x := 0 to terrain.TileWidth - 1 do
|
|
render_blocks.Add(@blocks[y, x]);
|
|
render_blocks.Sort(@TerrainBlockOrderByTex);
|
|
|
|
SetLength(render_batches, terrain.heightmap.texture_count);
|
|
start_idx := 0;
|
|
end_idx := 0;
|
|
for tex := 0 to terrain.heightmap.texture_count - 1 do begin
|
|
render_batches[tex].texture_gl_index := GenTexture(tex, true);
|
|
|
|
while PTerrainBlock(render_blocks.Items[end_idx])^.texture_index = tex do begin
|
|
end_idx += 1;
|
|
if end_idx > render_blocks.Count - 1 then
|
|
break;
|
|
end;
|
|
block_count := end_idx - start_idx;
|
|
render_batches[tex].blocks := block_count;
|
|
if block_count = 0 then
|
|
continue;
|
|
|
|
element_array_size := block_count * VerticesPerBlock * sizeof(Tvector3_single);
|
|
render_batches[tex].vertices := getmem (element_array_size);
|
|
render_batches[tex].normals := getmem (element_array_size);
|
|
render_batches[tex].face_indices := getmem (block_count * FacesPerBlock*3 * 4);
|
|
|
|
for k := 0 to block_count - 1 do begin
|
|
move(PTerrainBlock(render_blocks.Items[start_idx + k])^.vertices[0],
|
|
(pbyte(render_batches[tex].vertices) + k * VerticesPerBlock * sizeof(Tvector3_single))^,
|
|
VerticesPerBlock * sizeof(Tvector3_single));
|
|
move(PTerrainBlock(render_blocks.Items[start_idx + k])^.normals[0],
|
|
(pbyte(render_batches[tex].normals) + k * VerticesPerBlock * sizeof(Tvector3_single))^,
|
|
VerticesPerBlock * sizeof(Tvector3_single));
|
|
for vidx := 0 to FacesPerBlock*3 - 1 do
|
|
render_batches[tex].face_indices[k * FacesPerBlock*3 + vidx] :=
|
|
block_face_indices[vidx] + k * VerticesPerBlock;
|
|
end;
|
|
|
|
start_idx := end_idx;
|
|
if end_idx > render_blocks.Count - 1 then
|
|
break;
|
|
end;
|
|
render_blocks.Free;
|
|
end;
|
|
|
|
|
|
{ DrawGL
|
|
Renders terrain block batches.
|
|
}
|
|
procedure TTerrainMesh.DrawGL(opts: TRenderOpts);
|
|
var
|
|
i: integer;
|
|
b: TRenderBlockBatch;
|
|
begin
|
|
if opts.wireframe then
|
|
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
|
|
else
|
|
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
|
|
|
|
glEnable(GL_TEXTURE_2D);
|
|
glEnableClientState(GL_VERTEX_ARRAY);
|
|
glEnableClientState(GL_NORMAL_ARRAY);
|
|
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
|
|
glTexCoordPointer(2, GL_FLOAT, sizeof(Tvector2_single), @block_texcoords[0].data[0]);
|
|
|
|
for i := 0 to Length(render_batches) - 1 do begin
|
|
b := render_batches[i];
|
|
if b.blocks = 0 then
|
|
continue;
|
|
|
|
glBindTexture(GL_TEXTURE_2D, b.texture_gl_index);
|
|
glVertexPointer(3, GL_FLOAT, sizeof(Tvector3_single), b.vertices);
|
|
glNormalPointer(GL_FLOAT, sizeof(Tvector3_single), b.normals);
|
|
|
|
if opts.points then
|
|
glDrawArrays(GL_POINTS, 0, b.blocks * VerticesPerBlock)
|
|
else
|
|
glDrawElements(GL_TRIANGLES, b.blocks * FacesPerBlock * 3, GL_UNSIGNED_INT, b.face_indices);
|
|
end;
|
|
if opts.wireframe then
|
|
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
|
|
end;
|
|
|
|
end.
|
|
|