Create scaffolding of batched rendering for tile layer

This commit is contained in:
2025-07-17 22:17:35 +02:00
parent 380d2cd254
commit f131f9ef7f
16 changed files with 696 additions and 72 deletions

View File

@@ -1,7 +1,6 @@
package com.bartlomiejpluta.base.api.camera;
import com.bartlomiejpluta.base.api.context.Context;
import com.bartlomiejpluta.base.api.move.Movable;
import com.bartlomiejpluta.base.api.screen.Screen;
import com.bartlomiejpluta.base.internal.object.Placeable;
import com.bartlomiejpluta.base.internal.render.BoundingBox;
@@ -11,6 +10,8 @@ import org.joml.Matrix4fc;
public interface Camera extends Placeable, BoundingBox {
Matrix4fc computeViewModelMatrix(Matrix4fc modelMatrix);
Matrix4fc getProjectionMatrix();
boolean insideFrustum(float x, float y, float radius);
boolean insideFrustum(Context context, float x, float y);

View File

@@ -5,7 +5,5 @@ import com.bartlomiejpluta.base.api.map.layer.base.Layer;
public interface TileLayer extends Layer {
void setTile(int row, int column, int tileId);
void setTile(int row, int column, int tileSetRow, int tileSetColumn);
void clearTile(int row, int column);
}

View File

@@ -0,0 +1,288 @@
package com.bartlomiejpluta.base.engine.core.gl.object.mesh;
import com.bartlomiejpluta.base.api.camera.Camera;
import com.bartlomiejpluta.base.api.screen.Screen;
import com.bartlomiejpluta.base.internal.gc.Disposable;
import com.bartlomiejpluta.base.internal.render.Renderable;
import com.bartlomiejpluta.base.internal.render.ShaderManager;
import lombok.Getter;
import org.lwjgl.opengl.GL15;
import org.lwjgl.system.MemoryStack;
import java.util.ArrayList;
import java.util.List;
import static org.lwjgl.opengl.GL15.*;
import static org.lwjgl.opengl.GL20.*;
import static org.lwjgl.opengl.GL30.*;
public class BatchedQuads implements Renderable, Disposable {
// Texture: Quad:
// (0,1) ---- (1,1) 1 ----------- 2
// | | | |
// | | => | TEXTURE |
// | | | |
// (0,0) ---- (1,0) 0 ----------- 3
private static final float[] DEFAULT_TEXTURE_COORDINATES = new float[] {
0, 0, // 0 - bottom left
0, 1, // 1 - top left
1, 1, // 2 - top right
1, 0 // 3 - bottom right
};
private static final int POSITION_VECTOR_LAYOUT_INDEX = 0;
private static final int POSITION_VECTOR_LENGTH = 2;
private static final int TEXTURE_COORDINATES_VECTOR_LAYOUT_INDEX = 1;
private static final int TEXTURE_COORDINATES_VECTOR_LENGTH = 2;
private static final int VERTICES_PER_QUAD = 4;
private static final int INDICES_PER_QUAD = 6;
private final int maxQuads;
private final int vaoId;
private final List<Integer> vboIds = new ArrayList<>(3);
private final int maxVertices;
private final int maxIndices;
private final float[] vertexBuffer;
private final float[] texCoordBuffer;
private final int[] indexBuffer;
@Getter
private int currentQuadCount = 0;
public BatchedQuads(int maxQuads) {
this.maxQuads = maxQuads;
this.maxVertices = maxQuads * VERTICES_PER_QUAD * 2;
this.maxIndices = maxQuads * INDICES_PER_QUAD;
this.vertexBuffer = new float[maxVertices];
this.texCoordBuffer = new float[maxVertices];
this.indexBuffer = new int[maxIndices];
try (var stack = MemoryStack.stackPush()) {
vaoId = glGenVertexArrays();
glBindVertexArray(vaoId);
// Vertex buffer
var vertexVboId = glGenBuffers();
vboIds.add(vertexVboId);
glBindBuffer(GL_ARRAY_BUFFER, vertexVboId);
glBufferData(GL_ARRAY_BUFFER, maxVertices * Float.BYTES, GL_DYNAMIC_DRAW);
glVertexAttribPointer(POSITION_VECTOR_LAYOUT_INDEX, POSITION_VECTOR_LENGTH, GL_FLOAT, false, 0, 0);
glEnableVertexAttribArray(0);
// Texture coordinates buffer
var texCoordVboId = glGenBuffers();
vboIds.add(texCoordVboId);
glBindBuffer(GL_ARRAY_BUFFER, texCoordVboId);
glBufferData(GL_ARRAY_BUFFER, maxVertices * Float.BYTES, GL_DYNAMIC_DRAW);
glVertexAttribPointer(TEXTURE_COORDINATES_VECTOR_LAYOUT_INDEX, TEXTURE_COORDINATES_VECTOR_LENGTH, GL_FLOAT, false, 0, 0);
glEnableVertexAttribArray(1);
// Index buffer
var indexVboId = glGenBuffers();
vboIds.add(indexVboId);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexVboId);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, maxIndices * Integer.BYTES, GL_DYNAMIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}
}
public int addQuad(Quad quad, float x, float y) {
return addQuad(quad, x, y, DEFAULT_TEXTURE_COORDINATES);
}
public int addQuad(Quad quad, float x, float y, float[] textureCoordinates) {
if (currentQuadCount >= maxQuads) {
throw new RuntimeException("Batch is full!");
}
int quadId = currentQuadCount;
// Vertices
var localVertices = quad.getVertices();
int vertexStartIndex = currentQuadCount * VERTICES_PER_QUAD * 2;
for (int i = 0; i < VERTICES_PER_QUAD; i++) {
float localX = localVertices[i * 2];
float localY = localVertices[i * 2 + 1];
vertexBuffer[vertexStartIndex + i * 2] = x + localX;
vertexBuffer[vertexStartIndex + i * 2 + 1] = y + localY;
}
// Texture
var texCoordStartIndex = currentQuadCount * VERTICES_PER_QUAD * 2;
System.arraycopy(textureCoordinates, 0, texCoordBuffer, texCoordStartIndex, textureCoordinates.length);
// Indices
var indexStartIndex = currentQuadCount * INDICES_PER_QUAD;
var vertexOffset = currentQuadCount * VERTICES_PER_QUAD;
int[] quadIndices = {
// First triangle: indices 0-1-2
// 1 ----------- 2
// |XXXXXXXXXXX |
// |XXXXXXXX |
// |XXXXX |
// |XX |
// 0 ----------- 3
vertexOffset + 0, vertexOffset + 1, vertexOffset + 2,
// Second triangle: indices 2-3-0
// 1 ----------- 2
// | X|
// | XXXX|
// | XXXXXXX|
// | XXXXXXXXXX|
// 0 ----------- 3
vertexOffset + 2, vertexOffset + 3, vertexOffset + 0
};
System.arraycopy(quadIndices, 0, indexBuffer, indexStartIndex, quadIndices.length);
currentQuadCount++;
return quadId;
}
public void setQuadTextureCoordinates(int quadId, float[] textureCoordinates) {
if (quadId < 0 || quadId >= currentQuadCount) {
throw new IllegalArgumentException("Invalid quad ID: " + quadId);
}
var texCoordsIndex = quadId * VERTICES_PER_QUAD * 2;
System.arraycopy(textureCoordinates, 0, texCoordBuffer, texCoordsIndex, textureCoordinates.length);
}
public void clear() {
currentQuadCount = 0;
}
public void removeQuad(int quadId) {
if (quadId < 0 || quadId >= currentQuadCount) {
throw new IllegalArgumentException("Invalid quad ID: " + quadId);
}
int lastQuadId = currentQuadCount - 1;
if (quadId != lastQuadId) {
swapQuads(quadId, lastQuadId);
}
currentQuadCount--;
}
private void swapQuads(int quad1, int quad2) {
// Swap vertices
swapQuadData(vertexBuffer, quad1, quad2, VERTICES_PER_QUAD * 2);
// Swap texture coordinates
swapQuadData(texCoordBuffer, quad1, quad2, VERTICES_PER_QUAD * 2);
// Swap indices
swapQuadIndices(quad1, quad2);
}
private void swapQuadData(float[] buffer, int quad1, int quad2, int dataSize) {
var start1 = quad1 * dataSize;
var start2 = quad2 * dataSize;
for (int i = 0; i < dataSize; i++) {
var temp = buffer[start1 + i];
buffer[start1 + i] = buffer[start2 + i];
buffer[start2 + i] = temp;
}
}
private void swapQuadIndices(int quad1, int quad2) {
var start1 = quad1 * INDICES_PER_QUAD;
var start2 = quad2 * INDICES_PER_QUAD;
var vertexOffset1 = quad1 * VERTICES_PER_QUAD;
int[] newIndices1 = {
vertexOffset1 + 0, vertexOffset1 + 1, vertexOffset1 + 2,
vertexOffset1 + 2, vertexOffset1 + 3, vertexOffset1 + 0
};
var vertexOffset2 = quad2 * VERTICES_PER_QUAD;
int[] newIndices2 = {
vertexOffset2 + 0, vertexOffset2 + 1, vertexOffset2 + 2,
vertexOffset2 + 2, vertexOffset2 + 3, vertexOffset2 + 0
};
System.arraycopy(newIndices1, 0, indexBuffer, start1, INDICES_PER_QUAD);
System.arraycopy(newIndices2, 0, indexBuffer, start2, INDICES_PER_QUAD);
}
@Override
public void render(Screen screen, Camera camera, ShaderManager shaderManager) {
if (currentQuadCount == 0) {
return;
}
glBindVertexArray(vaoId);
try (var stack = MemoryStack.stackPush()) {
// Vertex buffer
glBindBuffer(GL_ARRAY_BUFFER, vboIds.get(0));
var vertexFloatBuffer = stack.mallocFloat(currentQuadCount * VERTICES_PER_QUAD * 2);
for (int i = 0; i < currentQuadCount * VERTICES_PER_QUAD * 2; i++) {
vertexFloatBuffer.put(vertexBuffer[i]);
}
vertexFloatBuffer.flip();
glBufferSubData(GL_ARRAY_BUFFER, 0, vertexFloatBuffer);
// Texture coordinate buffer
glBindBuffer(GL_ARRAY_BUFFER, vboIds.get(1));
var texCoordFloatBuffer = stack.mallocFloat(currentQuadCount * VERTICES_PER_QUAD * 2);
for (int i = 0; i < currentQuadCount * VERTICES_PER_QUAD * 2; i++) {
texCoordFloatBuffer.put(texCoordBuffer[i]);
}
texCoordFloatBuffer.flip();
glBufferSubData(GL_ARRAY_BUFFER, 0, texCoordFloatBuffer);
// Index buffer
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIds.get(2));
var indexIntBuffer = stack.mallocInt(currentQuadCount * INDICES_PER_QUAD);
for (int i = 0; i < currentQuadCount * INDICES_PER_QUAD; i++) {
indexIntBuffer.put(indexBuffer[i]);
}
indexIntBuffer.flip();
glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, indexIntBuffer);
}
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glDrawElements(GL_TRIANGLES, currentQuadCount * INDICES_PER_QUAD, GL_UNSIGNED_INT, 0);
glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);
glBindVertexArray(0);
}
@Override
public void dispose() {
glDisableVertexAttribArray(0);
glDisableVertexAttribArray(1);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
vboIds.forEach(GL15::glDeleteBuffers);
glBindVertexArray(0);
glDeleteVertexArrays(vaoId);
}
public boolean isFull() {
return currentQuadCount >= maxQuads;
}
public boolean isEmpty() {
return currentQuadCount == 0;
}
}

View File

@@ -0,0 +1,62 @@
package com.bartlomiejpluta.base.engine.core.gl.object.mesh;
import lombok.Getter;
@Getter
public class Quad {
private final float width;
private final float height;
private final float originX;
private final float originY;
private final float[] vertices;
public Quad(float width, float height, float originX, float originY) {
this.width = width;
this.height = height;
this.originX = originX;
this.originY = originY;
// 1 ----------- 2
// | |
// | |
// | |
// | |
// | |
// 0 ----------- 3
this.vertices = new float[]{
-originX, -originY, // 0 - bottom left
-originX, height - originY, // 1 - top left
width - originX, height - originY, // 2 - top right
width - originX, -originY // 3 - bottom right
};
}
static float[] textureCoordinates = new float[]{
0, 0, // 0 - bottom left
0, 1, // 1 - top left
1, 1, // 2 - top right
1, 0 // 3 - bottom right
};
static int[] elements = new int[]{
// First triangle: indices 0-1-2
// 1 ----------- 2
// |XXXXXXXXXXX |
// |XXXXXXXX |
// |XXXXX |
// |XX |
// 0 ----------- 3
0, 1, 2,
// Second triangle: indices 2-3-0
// 1 ----------- 2
// | X|
// | XXXX|
// | XXXXXXX|
// | XXXXXXXXXX|
// 0 ----------- 3
2, 3, 0
};
}

View File

@@ -74,6 +74,31 @@ public class Texture implements Disposable {
}
}
public float[] getTextureCoordinates(int id) {
return getTextureCoordinates(id % columns, id / columns);
}
public float[] getTextureCoordinates(int x, int y) {
return getTextureCoordinates(x * (int) spriteSize.x(), y * (int) spriteSize.y(), (int) spriteSize.x(), (int) spriteSize.y());
}
public float[] getTextureCoordinates(int textureX, int textureY, int tileWidth, int tileHeight) {
var normalizedX = (float) textureX / width;
var normalizedY = (float) textureY / height;
var normalizedWidth = (float) tileWidth / width;
var normalizedHeight = (float) tileHeight / height;
var xEnd = (normalizedX + normalizedWidth);
var yEnd = (normalizedY + normalizedHeight);
return new float[]{
normalizedX, normalizedY, // bottom left
normalizedX, yEnd, // top left
xEnd, yEnd, // top right
xEnd, normalizedY // bottom right
};
}
public void activate() {
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textureId);

View File

@@ -2,10 +2,7 @@ package com.bartlomiejpluta.base.engine.core.gl.render;
import com.bartlomiejpluta.base.api.camera.Camera;
import com.bartlomiejpluta.base.api.screen.Screen;
import com.bartlomiejpluta.base.engine.core.gl.shader.constant.CounterName;
import com.bartlomiejpluta.base.engine.core.gl.shader.constant.RenderConstants;
import com.bartlomiejpluta.base.engine.core.gl.shader.constant.Shader;
import com.bartlomiejpluta.base.engine.core.gl.shader.constant.UniformName;
import com.bartlomiejpluta.base.internal.render.Renderable;
import com.bartlomiejpluta.base.internal.render.ShaderManager;
import lombok.RequiredArgsConstructor;
@@ -13,8 +10,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import static java.util.Arrays.stream;
import static org.lwjgl.opengl.GL15.*;
@Slf4j
@@ -28,32 +24,7 @@ public class DefaultRenderer implements Renderer {
log.info("Initializing renderer");
log.info("Registering shaders");
for (var shader : Shader.values()) {
shaderManager.createShader(shader.name, shader.vertex, shader.fragment);
}
log.info("Registering uniforms");
shaderManager
.activateShader(Shader.DEFAULT.name)
.createUniform(UniformName.UNI_VIEW_MODEL_MATRIX)
.createUniform(UniformName.UNI_MODEL_MATRIX)
.createUniform(UniformName.UNI_PROJECTION_MATRIX)
.createUniform(UniformName.UNI_OBJECT_COLOR)
.createUniform(UniformName.UNI_HAS_OBJECT_TEXTURE)
.createUniform(UniformName.UNI_TEXTURE_SAMPLER)
.createUniform(UniformName.UNI_SPRITE_SIZE)
.createUniform(UniformName.UNI_SPRITE_POSITION)
.createUniform(UniformName.UNI_AMBIENT)
.createUniform(UniformName.UNI_ACTIVE_LIGHTS)
.createCounter(CounterName.LIGHT);
for(int i=0; i<RenderConstants.MAX_LIGHTS; ++i) {
shaderManager.createUniform(UniformName.UNI_LIGHTS + "[" + i + "].position");
shaderManager.createUniform(UniformName.UNI_LIGHTS + "[" + i + "].intensity");
shaderManager.createUniform(UniformName.UNI_LIGHTS + "[" + i + "].constantAttenuation");
shaderManager.createUniform(UniformName.UNI_LIGHTS + "[" + i + "].linearAttenuation");
shaderManager.createUniform(UniformName.UNI_LIGHTS + "[" + i + "].quadraticAttenuation");
}
stream(Shader.values()).forEach(shader -> shader.init(shaderManager));
}
@Override
@@ -69,6 +40,7 @@ public class DefaultRenderer implements Renderer {
// due to the fact, that the method updates projection and view matrices, that
// are used to compute proper vertex coordinates of rendered objects (renderables).
camera.render(screen, shaderManager);
renderable.render(screen, camera, shaderManager);
shaderManager.deactivateShader();

View File

@@ -1,12 +1,50 @@
package com.bartlomiejpluta.base.engine.core.gl.shader.constant;
import com.bartlomiejpluta.base.internal.render.ShaderManager;
import lombok.RequiredArgsConstructor;
import java.util.function.Consumer;
@RequiredArgsConstructor
public enum Shader {
DEFAULT("default", "/shaders/default.vs", "/shaders/default.fs");
DEFAULT("default", "/shaders/default.vs", "/shaders/default.fs", shaderManager -> {
shaderManager.createUniform(UniformName.UNI_VIEW_MODEL_MATRIX)
.createUniform(UniformName.UNI_MODEL_MATRIX)
.createUniform(UniformName.UNI_PROJECTION_MATRIX)
.createUniform(UniformName.UNI_OBJECT_COLOR)
.createUniform(UniformName.UNI_HAS_OBJECT_TEXTURE)
.createUniform(UniformName.UNI_TEXTURE_SAMPLER)
.createUniform(UniformName.UNI_SPRITE_SIZE)
.createUniform(UniformName.UNI_SPRITE_POSITION)
.createUniform(UniformName.UNI_AMBIENT)
.createUniform(UniformName.UNI_ACTIVE_LIGHTS)
.createCounter(CounterName.LIGHT);
for (int i = 0; i < RenderConstants.MAX_LIGHTS; ++i) {
shaderManager.createUniform(UniformName.UNI_LIGHTS + "[" + i + "].position");
shaderManager.createUniform(UniformName.UNI_LIGHTS + "[" + i + "].intensity");
shaderManager.createUniform(UniformName.UNI_LIGHTS + "[" + i + "].constantAttenuation");
shaderManager.createUniform(UniformName.UNI_LIGHTS + "[" + i + "].linearAttenuation");
shaderManager.createUniform(UniformName.UNI_LIGHTS + "[" + i + "].quadraticAttenuation");
}
}),
BATCH("batch", "/shaders/batch.vs", "/shaders/batch.fs", shaderManager -> {
shaderManager
.createUniform(UniformName.UNI_VIEW_MODEL_MATRIX)
.createUniform(UniformName.UNI_PROJECTION_MATRIX)
.createUniform(UniformName.UNI_TEXTURE_SAMPLER);
});
public final String name;
public final String vertex;
public final String fragment;
private final String vertex;
private final String fragment;
private final Consumer<ShaderManager> activator;
public void init(ShaderManager manager) {
manager.createShader(name, vertex, fragment);
manager.activateShader(name);
this.activator.accept(manager);
manager.deactivateShader();
}
}

View File

@@ -55,7 +55,11 @@ public class DefaultShaderManager implements ShaderManager {
current.detach();
stack.pop();
current = stack.peek();
current.use();
if (current != null) {
current.use();
}
return this;
}
@@ -152,7 +156,7 @@ public class DefaultShaderManager implements ShaderManager {
@Override
public ShaderManager createCounter(String counterName) {
if (counters.containsKey(counterName)) {
throw new AppException(format("The [%s] counter already exists", counterName));
return this;
}
log.info("Creating {} uniform counter", counterName);

View File

@@ -3,7 +3,6 @@ package com.bartlomiejpluta.base.engine.world.camera;
import com.bartlomiejpluta.base.api.camera.Camera;
import com.bartlomiejpluta.base.api.context.Context;
import com.bartlomiejpluta.base.api.screen.Screen;
import com.bartlomiejpluta.base.engine.core.gl.shader.constant.UniformName;
import com.bartlomiejpluta.base.engine.world.object.Model;
import com.bartlomiejpluta.base.internal.render.ShaderManager;
import lombok.Getter;
@@ -12,6 +11,7 @@ import org.joml.Matrix4f;
import org.joml.Matrix4fc;
public class DefaultCamera extends Model implements Camera {
@Getter
private final Matrix4f projectionMatrix = new Matrix4f();
private final Matrix4f viewMatrix = new Matrix4f();
private final Matrix4f projectionViewMatrix = new Matrix4f();
@@ -71,7 +71,5 @@ public class DefaultCamera extends Model implements Camera {
this.maxX = position.x + screenWidth / scaleX;
this.minY = position.y;
this.maxY = position.y + screenHeight / scaleY;
shaderManager.setUniform(UniformName.UNI_PROJECTION_MATRIX, projectionMatrix);
}
}

View File

@@ -0,0 +1,124 @@
package com.bartlomiejpluta.base.engine.world.map.layer.tile;
import com.bartlomiejpluta.base.api.camera.Camera;
import com.bartlomiejpluta.base.api.screen.Screen;
import com.bartlomiejpluta.base.engine.core.gl.object.texture.Texture;
import com.bartlomiejpluta.base.internal.gc.Disposable;
import com.bartlomiejpluta.base.internal.render.Renderable;
import com.bartlomiejpluta.base.internal.render.ShaderManager;
import java.util.HashMap;
import java.util.Map;
public class ChunkManager implements Renderable, Disposable {
private static final int DEFAULT_CHUNK_SIZE = 10;
private final Map<Long, TileChunk> chunks = new HashMap<>();
private final Map<Long, Map<Integer, Integer>> chunkTileIds = new HashMap<>();
private final Texture tileSet;
private final int chunkSize;
private final float tileWidth, tileHeight;
private final int mapRows, mapColumns;
public ChunkManager(Texture tileSet, int rows, int columns) {
this(tileSet, rows, columns, DEFAULT_CHUNK_SIZE);
}
public ChunkManager(Texture tileSet, int rows, int columns, int chunkSize) {
this.tileSet = tileSet;
this.mapRows = rows;
this.mapColumns = columns;
this.tileWidth = tileSet.getSpriteSize().x();
this.tileHeight = tileSet.getSpriteSize().y();
this.chunkSize = chunkSize;
}
private long getChunkKey(int row, int column) {
var chunkX = column / chunkSize;
var chunkY = row / chunkSize;
return ((long) chunkX << 32) | (chunkY & 0xFFFFFFFFL);
}
private int getLocalTileKey(int row, int column) {
var localRow = row % chunkSize;
var localColumn = column % chunkSize;
return localRow * chunkSize + localColumn;
}
private TileChunk getOrCreateChunk(int row, int column) {
var chunkKey = getChunkKey(row, column);
var chunk = chunks.get(chunkKey);
if (chunk == null) {
int chunkX = (column / chunkSize) * (int) tileWidth * chunkSize;
int chunkY = (row / chunkSize) * (int) tileHeight * chunkSize;
chunk = new TileChunk(tileSet, chunkSize);
chunk.setPosition(chunkX, chunkY);
chunks.put(chunkKey, chunk);
chunkTileIds.put(chunkKey, new HashMap<>());
}
return chunk;
}
public void setTile(int row, int column, int tileId) {
if (row < 0 || row >= mapRows || column < 0 || column >= mapColumns) {
throw new IllegalArgumentException("Tile coordinates out of bounds");
}
clearTile(row, column);
var chunk = getOrCreateChunk(row, column);
var chunkKey = getChunkKey(row, column);
var localTileKey = getLocalTileKey(row, column);
var quadId = chunk.addTile(column % chunkSize * (int) this.tileHeight, row % chunkSize * (int) this.tileWidth, tileId);
chunkTileIds.get(chunkKey).put(localTileKey, quadId);
}
public void clearTile(int row, int column) {
if (row < 0 || row >= mapRows || column < 0 || column >= mapColumns) {
return;
}
var chunkKey = getChunkKey(row, column);
var localTileKey = getLocalTileKey(row, column);
var chunk = chunks.get(chunkKey);
if (chunk == null) {
return;
}
var tileMap = chunkTileIds.get(chunkKey);
var quadId = tileMap.remove(localTileKey);
if (quadId != null) {
chunk.removeTile(quadId);
if (chunk.isEmpty()) {
chunks.remove(chunkKey);
chunkTileIds.remove(chunkKey);
}
}
}
@Override
public void render(Screen screen, Camera camera, ShaderManager shaderManager) {
for (TileChunk chunk : chunks.values()) {
if (camera.containsBox(chunk)) {
chunk.render(screen, camera, shaderManager);
}
}
}
@Override
public void dispose() {
chunks.values().forEach(TileChunk::dispose);
chunks.clear();
chunkTileIds.clear();
}
}

View File

@@ -4,55 +4,36 @@ import com.bartlomiejpluta.base.api.camera.Camera;
import com.bartlomiejpluta.base.api.map.layer.tile.TileLayer;
import com.bartlomiejpluta.base.api.map.model.GameMap;
import com.bartlomiejpluta.base.api.screen.Screen;
import com.bartlomiejpluta.base.engine.core.gl.object.texture.Texture;
import com.bartlomiejpluta.base.engine.world.map.layer.base.BaseLayer;
import com.bartlomiejpluta.base.engine.world.tileset.model.Tile;
import com.bartlomiejpluta.base.engine.world.tileset.model.TileSet;
import com.bartlomiejpluta.base.internal.render.ShaderManager;
import lombok.NonNull;
import java.util.Arrays;
public class DefaultTileLayer extends BaseLayer implements TileLayer {
private final TileSet tileSet;
private final Tile[][] layer;
private final Texture tileSet;
private final ChunkManager chunkManager;
public DefaultTileLayer(@NonNull GameMap map, @NonNull TileSet tileSet, int rows, int columns) {
super(map);
this.tileSet = tileSet;
layer = new Tile[rows][columns];
Arrays.stream(layer).forEach(tiles -> Arrays.fill(tiles, null));
this.tileSet = tileSet.getTileSet();
this.chunkManager = new ChunkManager(tileSet.getTileSet(), rows, columns);
}
@Override
public void setTile(int row, int column, int tileId) {
var tile = tileSet.tileById(tileId);
tile.setLocation(row, column);
layer[row][column] = tile;
}
@Override
public void setTile(int row, int column, int tileSetRow, int tileSetColumn) {
var tile = tileSet.tileAt(tileSetRow, tileSetColumn);
tile.setLocation(row, column);
layer[row][column] = tile;
chunkManager.setTile(row, column, tileId);
}
@Override
public void clearTile(int row, int column) {
layer[row][column] = null;
chunkManager.clearTile(row, column);
}
@Override
public void render(Screen screen, Camera camera, ShaderManager shaderManager) {
for (var row : layer) {
for (var tile : row) {
if (tile != null) {
tile.render(screen, camera, shaderManager);
}
}
}
super.render(screen, camera, shaderManager);
chunkManager.render(screen, camera, shaderManager);
}
}

View File

@@ -0,0 +1,94 @@
package com.bartlomiejpluta.base.engine.world.map.layer.tile;
import com.bartlomiejpluta.base.api.camera.Camera;
import com.bartlomiejpluta.base.api.screen.Screen;
import com.bartlomiejpluta.base.engine.core.gl.object.mesh.BatchedQuads;
import com.bartlomiejpluta.base.engine.core.gl.object.mesh.Quad;
import com.bartlomiejpluta.base.engine.core.gl.object.texture.Texture;
import com.bartlomiejpluta.base.engine.core.gl.shader.constant.Shader;
import com.bartlomiejpluta.base.engine.core.gl.shader.constant.UniformName;
import com.bartlomiejpluta.base.engine.world.object.Model;
import com.bartlomiejpluta.base.internal.gc.Disposable;
import com.bartlomiejpluta.base.internal.object.Placeable;
import com.bartlomiejpluta.base.internal.render.BoundingBox;
import com.bartlomiejpluta.base.internal.render.Renderable;
import com.bartlomiejpluta.base.internal.render.ShaderManager;
public class TileChunk extends Model implements Placeable, Renderable, Disposable, BoundingBox {
private final Texture tileSet;
private final BatchedQuads mesh;
private final int chunkSize;
private final Quad quad;
private final float originX;
private final float originY;
public TileChunk(Texture tileSet, int chunkSize) {
this(tileSet, chunkSize, 0, 0);
}
public TileChunk(Texture tileSet, int chunkSize, float originX, float originY) {
this.tileSet = tileSet;
this.chunkSize = chunkSize;
this.mesh = new BatchedQuads(chunkSize * chunkSize);
this.quad = new Quad(tileSet.getSpriteSize().x(), tileSet.getSpriteSize().y(), originX, originY);
this.originX = originX;
this.originY = originY;
}
public int addTile(int x, int y, int tileId) {
return mesh.addQuad(quad, x, y, tileSet.getTextureCoordinates(tileId));
}
public void removeTile(int quadId) {
mesh.removeQuad(quadId);
}
public boolean isEmpty() {
return mesh.isEmpty();
}
@Override
public void render(Screen screen, Camera camera, ShaderManager shaderManager) {
tileSet.activate();
shaderManager.activateShader(Shader.BATCH.name);
shaderManager.setUniform(UniformName.UNI_PROJECTION_MATRIX, camera.getProjectionMatrix());
shaderManager.setUniform(UniformName.UNI_VIEW_MODEL_MATRIX, camera.computeViewModelMatrix(getModelMatrix()));
shaderManager.setUniform(UniformName.UNI_TEXTURE_SAMPLER, 0);
mesh.render(screen, camera, shaderManager);
shaderManager.deactivateShader();
}
@Override
public float getMinX() {
float scaledOriginX = originX * scaleX;
return getPosition().x() - scaledOriginX;
}
@Override
public float getMaxX() {
float scaledOriginX = originX * scaleX;
float scaledChunkWidth = chunkSize * tileSet.getSpriteSize().x() *scaleX;
return getPosition().x() + scaledChunkWidth - scaledOriginX;
}
@Override
public float getMinY() {
float scaledOriginY = originY * scaleY;
return getPosition().y() - scaledOriginY;
}
@Override
public float getMaxY() {
float scaledOriginY = originY * scaleY;
float scaledChunkHeight = chunkSize * tileSet.getSpriteSize().y() * scaleY;
return getPosition().y() + scaledChunkHeight - scaledOriginY;
}
@Override
public void dispose() {
mesh.dispose();
}
}

View File

@@ -37,6 +37,7 @@ public abstract class Sprite extends LocationableModel implements Renderable {
return;
}
shaderManager.setUniform(UniformName.UNI_PROJECTION_MATRIX, camera.getProjectionMatrix());
shaderManager.setUniform(UniformName.UNI_VIEW_MODEL_MATRIX, camera.computeViewModelMatrix(getModelMatrix()));
shaderManager.setUniform(UniformName.UNI_MODEL_MATRIX, getModelMatrix());
material.render(screen, camera, shaderManager);

View File

@@ -1,15 +1,23 @@
package com.bartlomiejpluta.base.engine.world.tileset.model;
import com.bartlomiejpluta.base.engine.core.gl.object.material.Material;
import com.bartlomiejpluta.base.engine.core.gl.object.mesh.Mesh;
import com.bartlomiejpluta.base.engine.core.gl.object.texture.Texture;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class TileSet {
private final Texture tileSet;
private final Mesh mesh;
private final Material material;
public TileSet(@NonNull Texture tileSet, @NonNull Mesh mesh) {
this.tileSet = tileSet;
this.mesh = mesh;
this.material = Material.textured(tileSet);
}
public Tile tileById(int id) {
return new Tile(mesh, tileSet, id);

View File

@@ -0,0 +1,15 @@
#version 330
in vec2 fragmentTexCoord;
uniform sampler2D sampler;
out vec4 fragColor;
void main() {
fragColor = texture(sampler, fragmentTexCoord);
if (fragColor.a < 0.1) {
discard;
}
}

View File

@@ -0,0 +1,15 @@
#version 330
uniform mat4 viewModelMatrix;
uniform mat4 projectionMatrix;
layout(location=0) in vec2 position;
layout(location=1) in vec2 texCoord;
out vec2 fragmentTexCoord;
void main()
{
gl_Position = projectionMatrix * viewModelMatrix * vec4(position, 0.0, 1.0);
fragmentTexCoord = texCoord;
}