diff --git a/api/src/main/java/com/bartlomiejpluta/base/api/camera/Camera.java b/api/src/main/java/com/bartlomiejpluta/base/api/camera/Camera.java index 3e56afbe..cc56413b 100644 --- a/api/src/main/java/com/bartlomiejpluta/base/api/camera/Camera.java +++ b/api/src/main/java/com/bartlomiejpluta/base/api/camera/Camera.java @@ -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); diff --git a/api/src/main/java/com/bartlomiejpluta/base/api/map/layer/tile/TileLayer.java b/api/src/main/java/com/bartlomiejpluta/base/api/map/layer/tile/TileLayer.java index 4e3fdc59..07142a2c 100644 --- a/api/src/main/java/com/bartlomiejpluta/base/api/map/layer/tile/TileLayer.java +++ b/api/src/main/java/com/bartlomiejpluta/base/api/map/layer/tile/TileLayer.java @@ -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); } diff --git a/engine/src/main/java/com/bartlomiejpluta/base/engine/core/gl/object/mesh/BatchedQuads.java b/engine/src/main/java/com/bartlomiejpluta/base/engine/core/gl/object/mesh/BatchedQuads.java new file mode 100644 index 00000000..3d63faa2 --- /dev/null +++ b/engine/src/main/java/com/bartlomiejpluta/base/engine/core/gl/object/mesh/BatchedQuads.java @@ -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 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; + } +} diff --git a/engine/src/main/java/com/bartlomiejpluta/base/engine/core/gl/object/mesh/Quad.java b/engine/src/main/java/com/bartlomiejpluta/base/engine/core/gl/object/mesh/Quad.java new file mode 100644 index 00000000..7da1c922 --- /dev/null +++ b/engine/src/main/java/com/bartlomiejpluta/base/engine/core/gl/object/mesh/Quad.java @@ -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 + }; +} diff --git a/engine/src/main/java/com/bartlomiejpluta/base/engine/core/gl/object/texture/Texture.java b/engine/src/main/java/com/bartlomiejpluta/base/engine/core/gl/object/texture/Texture.java index 8fed9077..d4492853 100644 --- a/engine/src/main/java/com/bartlomiejpluta/base/engine/core/gl/object/texture/Texture.java +++ b/engine/src/main/java/com/bartlomiejpluta/base/engine/core/gl/object/texture/Texture.java @@ -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); diff --git a/engine/src/main/java/com/bartlomiejpluta/base/engine/core/gl/render/DefaultRenderer.java b/engine/src/main/java/com/bartlomiejpluta/base/engine/core/gl/render/DefaultRenderer.java index 9428ec2a..4c1d30e3 100644 --- a/engine/src/main/java/com/bartlomiejpluta/base/engine/core/gl/render/DefaultRenderer.java +++ b/engine/src/main/java/com/bartlomiejpluta/base/engine/core/gl/render/DefaultRenderer.java @@ -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 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(); diff --git a/engine/src/main/java/com/bartlomiejpluta/base/engine/core/gl/shader/constant/Shader.java b/engine/src/main/java/com/bartlomiejpluta/base/engine/core/gl/shader/constant/Shader.java index 92f99448..edd7062d 100644 --- a/engine/src/main/java/com/bartlomiejpluta/base/engine/core/gl/shader/constant/Shader.java +++ b/engine/src/main/java/com/bartlomiejpluta/base/engine/core/gl/shader/constant/Shader.java @@ -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 activator; + + public void init(ShaderManager manager) { + manager.createShader(name, vertex, fragment); + manager.activateShader(name); + this.activator.accept(manager); + manager.deactivateShader(); + } } diff --git a/engine/src/main/java/com/bartlomiejpluta/base/engine/core/gl/shader/manager/DefaultShaderManager.java b/engine/src/main/java/com/bartlomiejpluta/base/engine/core/gl/shader/manager/DefaultShaderManager.java index 8dec4550..9a9bc03c 100644 --- a/engine/src/main/java/com/bartlomiejpluta/base/engine/core/gl/shader/manager/DefaultShaderManager.java +++ b/engine/src/main/java/com/bartlomiejpluta/base/engine/core/gl/shader/manager/DefaultShaderManager.java @@ -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); diff --git a/engine/src/main/java/com/bartlomiejpluta/base/engine/world/camera/DefaultCamera.java b/engine/src/main/java/com/bartlomiejpluta/base/engine/world/camera/DefaultCamera.java index 61d96bf6..f2e6296c 100644 --- a/engine/src/main/java/com/bartlomiejpluta/base/engine/world/camera/DefaultCamera.java +++ b/engine/src/main/java/com/bartlomiejpluta/base/engine/world/camera/DefaultCamera.java @@ -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); } } diff --git a/engine/src/main/java/com/bartlomiejpluta/base/engine/world/map/layer/tile/ChunkManager.java b/engine/src/main/java/com/bartlomiejpluta/base/engine/world/map/layer/tile/ChunkManager.java new file mode 100644 index 00000000..b004d555 --- /dev/null +++ b/engine/src/main/java/com/bartlomiejpluta/base/engine/world/map/layer/tile/ChunkManager.java @@ -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 chunks = new HashMap<>(); + private final Map> 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(); + } +} diff --git a/engine/src/main/java/com/bartlomiejpluta/base/engine/world/map/layer/tile/DefaultTileLayer.java b/engine/src/main/java/com/bartlomiejpluta/base/engine/world/map/layer/tile/DefaultTileLayer.java index 68421489..9d3706b6 100644 --- a/engine/src/main/java/com/bartlomiejpluta/base/engine/world/map/layer/tile/DefaultTileLayer.java +++ b/engine/src/main/java/com/bartlomiejpluta/base/engine/world/map/layer/tile/DefaultTileLayer.java @@ -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); } } diff --git a/engine/src/main/java/com/bartlomiejpluta/base/engine/world/map/layer/tile/TileChunk.java b/engine/src/main/java/com/bartlomiejpluta/base/engine/world/map/layer/tile/TileChunk.java new file mode 100644 index 00000000..df1179a7 --- /dev/null +++ b/engine/src/main/java/com/bartlomiejpluta/base/engine/world/map/layer/tile/TileChunk.java @@ -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(); + } +} diff --git a/engine/src/main/java/com/bartlomiejpluta/base/engine/world/object/Sprite.java b/engine/src/main/java/com/bartlomiejpluta/base/engine/world/object/Sprite.java index 27d037e2..29c321fe 100644 --- a/engine/src/main/java/com/bartlomiejpluta/base/engine/world/object/Sprite.java +++ b/engine/src/main/java/com/bartlomiejpluta/base/engine/world/object/Sprite.java @@ -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); diff --git a/engine/src/main/java/com/bartlomiejpluta/base/engine/world/tileset/model/TileSet.java b/engine/src/main/java/com/bartlomiejpluta/base/engine/world/tileset/model/TileSet.java index abe0a16b..84cda751 100644 --- a/engine/src/main/java/com/bartlomiejpluta/base/engine/world/tileset/model/TileSet.java +++ b/engine/src/main/java/com/bartlomiejpluta/base/engine/world/tileset/model/TileSet.java @@ -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); diff --git a/engine/src/main/resources/shaders/batch.fs b/engine/src/main/resources/shaders/batch.fs new file mode 100644 index 00000000..483cc3a0 --- /dev/null +++ b/engine/src/main/resources/shaders/batch.fs @@ -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; + } +} diff --git a/engine/src/main/resources/shaders/batch.vs b/engine/src/main/resources/shaders/batch.vs new file mode 100644 index 00000000..5338568f --- /dev/null +++ b/engine/src/main/resources/shaders/batch.vs @@ -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; +}