diff --git a/getho_bold.ttf b/getho_bold.ttf new file mode 100644 index 0000000..49bc5de Binary files /dev/null and b/getho_bold.ttf differ diff --git a/gradleScripts/dependenciesClient.gradle b/gradleScripts/dependenciesClient.gradle index 1ba29f0..9e56d48 100644 --- a/gradleScripts/dependenciesClient.gradle +++ b/gradleScripts/dependenciesClient.gradle @@ -15,10 +15,14 @@ dependencies { implementation "org.lwjgl:lwjgl-openal" implementation "org.lwjgl:lwjgl-opengl" implementation "org.lwjgl:lwjgl-stb" + implementation "org.lwjgl:lwjgl-freetype" + implementation "org.lwjgl:lwjgl-harfbuzz" runtimeOnly "org.lwjgl:lwjgl::$lwjglNatives" runtimeOnly "org.lwjgl:lwjgl-glfw::$lwjglNatives" runtimeOnly "org.lwjgl:lwjgl-nanovg::$lwjglNatives" runtimeOnly "org.lwjgl:lwjgl-openal::$lwjglNatives" runtimeOnly "org.lwjgl:lwjgl-opengl::$lwjglNatives" runtimeOnly "org.lwjgl:lwjgl-stb::$lwjglNatives" + runtimeOnly "org.lwjgl:lwjgl-freetype::$lwjglNatives" + runtimeOnly "org.lwjgl:lwjgl-harfbuzz::$lwjglNatives" } \ No newline at end of file diff --git a/src/main/java/com/terminalvelocitycabbage/engine/client/ClientBase.java b/src/main/java/com/terminalvelocitycabbage/engine/client/ClientBase.java index 9323f12..b2f6394 100644 --- a/src/main/java/com/terminalvelocitycabbage/engine/client/ClientBase.java +++ b/src/main/java/com/terminalvelocitycabbage/engine/client/ClientBase.java @@ -7,6 +7,7 @@ import com.terminalvelocitycabbage.engine.client.renderer.materials.TextureCache; import com.terminalvelocitycabbage.engine.client.renderer.model.Mesh; import com.terminalvelocitycabbage.engine.client.renderer.model.Model; +import com.terminalvelocitycabbage.engine.client.ui.Font; import com.terminalvelocitycabbage.engine.client.window.InputCallbackListener; import com.terminalvelocitycabbage.engine.client.window.WindowManager; import com.terminalvelocitycabbage.engine.filesystem.resources.ResourceCategory; @@ -32,10 +33,11 @@ public abstract class ClientBase extends MainEntrypoint implements NetworkedSide private final WindowManager windowManager; private final Registry renderGraphRegistry; - //Scene stuff + //Rendering stuff protected final Registry meshRegistry; protected final Registry modelRegistry; protected TextureCache textureCache; + protected final Registry fontRegistry; //Networking stuff private final Client client; @@ -55,6 +57,7 @@ public ClientBase(String namespace, int ticksPerSecond) { inputCallbackListener = new InputCallbackListener(); meshRegistry = new Registry<>(); modelRegistry = new Registry<>(); + fontRegistry = new Registry<>(); client = new Client(); } @@ -92,6 +95,7 @@ public void init() { eventDispatcher.dispatchEvent(new EntityComponentRegistrationEvent(manager)); eventDispatcher.dispatchEvent(new EntitySystemRegistrationEvent(manager)); eventDispatcher.dispatchEvent(new RoutineRegistrationEvent(routineRegistry)); + eventDispatcher.dispatchEvent(new GenerateFontsEvent(fileSystem, fontRegistry)); var configureTexturesEvent = new ConfigureTexturesEvent(fileSystem); eventDispatcher.dispatchEvent(configureTexturesEvent); textureCache = new TextureCache(configureTexturesEvent.getTexturesToCompileToAtlas(), configureTexturesEvent.getSingleTextures()); @@ -216,4 +220,8 @@ public Registry getModelRegistry() { public TextureCache getTextureCache() { return textureCache; } + + public Registry getFontRegistry() { + return fontRegistry; + } } diff --git a/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/RenderGraph.java b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/RenderGraph.java index d820829..5c26192 100644 --- a/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/RenderGraph.java +++ b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/RenderGraph.java @@ -64,6 +64,9 @@ public void init(GLCapabilities capabilities) { initialized = true; this.capabilities = capabilities; + graphNodes.forEach((identifier, togglePair) -> { + if (togglePair.getValue1() instanceof RenderNode renderNode) renderNode.init(this); + }); } /** diff --git a/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/materials/FontAtlas.java b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/materials/FontAtlas.java new file mode 100644 index 0000000..8aca022 --- /dev/null +++ b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/materials/FontAtlas.java @@ -0,0 +1,73 @@ +package com.terminalvelocitycabbage.engine.client.renderer.materials; + +import com.terminalvelocitycabbage.engine.client.ui.Font; +import com.terminalvelocitycabbage.engine.debug.Log; +import com.terminalvelocitycabbage.engine.util.ImageUtils; +import com.terminalvelocitycabbage.engine.util.MathUtils; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.util.freetype.FT_Bitmap; +import org.lwjgl.util.freetype.FreeType; + +import java.io.File; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; + +public class FontAtlas extends Texture { + + private final Map glyphMap = new HashMap<>(); + + public FontAtlas(Font font, int[] glyphIds) { + + //Determine the likely size of atlas (cross the bridge when we get to it if we're wrong) + var atlasSize = MathUtils.findNearestPowerOfTwo((int) Math.ceil(Math.sqrt(font.getFontSize() * font.getFontSize() * glyphIds.length))); + this.width = atlasSize; + this.height = atlasSize; + + //Create the buffer for this atlas + ByteBuffer atlasBuffer = ByteBuffer.allocateDirect(this.width * this.height); + + int x = 0, y = 0, rowHeight = 0; + try (MemoryStack stack = MemoryStack.stackPush()) { + for (int glyphId : glyphIds) { + // Load glyph into FreeType + FreeType.FT_Load_Glyph(font.getFace(), glyphId, FreeType.FT_LOAD_RENDER); + FT_Bitmap bitmap = font.getFace().glyph().bitmap(); + + if (x + bitmap.width() >= getWidth()) { + x = 0; + y += rowHeight; + rowHeight = 0; + } + + // Copy glyph bitmap into atlas + ByteBuffer bitmapBuffer = bitmap.buffer(bitmap.rows() * bitmap.pitch()); + for (int row = 0; row < bitmap.rows(); row++) { + for (int col = 0; col < bitmap.width(); col++) { + atlasBuffer.put((y + row) * getWidth() + (x + col), bitmapBuffer.get(row * bitmap.pitch() + col)); + } + } + + // Record glyph info + glyphMap.put(glyphId, new GlyphInfo(x, y, bitmap.width(), bitmap.rows(), font.getFace().glyph().advance().x() / 64f)); + + x += bitmap.width() + 1; + rowHeight = Math.max(rowHeight, bitmap.rows()); + } + } + + // Upload to OpenGL + Log.info("generateOpenGLTexture"); + generateOpenGLTexture(width, height, 4, atlasBuffer); + + Log.info("Saving font atlas to file"); + ImageUtils.saveRGBAtoPNG(atlasBuffer, width, height, new File("font_atlas.png")); + } + + public GlyphInfo getGlyphInfo(int glyphId) { + return glyphMap.get(glyphId); + } + + public record GlyphInfo(int x, int y, int width, int height, float xAdvance) { } + +} diff --git a/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/materials/TextureCache.java b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/materials/TextureCache.java index 086f8ed..8bb99ec 100644 --- a/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/materials/TextureCache.java +++ b/src/main/java/com/terminalvelocitycabbage/engine/client/renderer/materials/TextureCache.java @@ -1,5 +1,6 @@ package com.terminalvelocitycabbage.engine.client.renderer.materials; +import com.terminalvelocitycabbage.engine.client.ClientBase; import com.terminalvelocitycabbage.engine.debug.Log; import com.terminalvelocitycabbage.engine.filesystem.resources.Resource; import com.terminalvelocitycabbage.engine.registry.Identifier; @@ -18,22 +19,29 @@ public class TextureCache { private final Map singleTextures; private final Map generatedTextures; + private final Map fontAtlasTextures; public TextureCache(Map> texturesToCompileToAtlas, Map singleTextures) { - this.generatedTextures = new HashMap<>(); this.texturesToCompileToAtlas = texturesToCompileToAtlas; + this.singleTextures = singleTextures; + this.generatedTextures = new HashMap<>(); + this.fontAtlasTextures = new HashMap<>(); } public void generateAtlas(Identifier atlasIdentifier) { var textures = texturesToCompileToAtlas.get(atlasIdentifier); var atlas = new Atlas(textures); for (Identifier textureId : textures.keySet()) { - Log.info("Generating texture " + textureId + " for atlas " + atlasIdentifier); this.generatedTextures.put(textureId, atlas); } } + public void generateFontAtlas(Identifier fontIdentifier, int[] glyphIds) { + Log.info("Generating font atlas for " + fontIdentifier); + this.fontAtlasTextures.put(fontIdentifier, new FontAtlas(ClientBase.getInstance().getFontRegistry().get(fontIdentifier), glyphIds)); + } + /** * @param texture The identifier for the texture you wish to retrieve from this cache * @return The requested texture or null (in the future this will return a default texture) diff --git a/src/main/java/com/terminalvelocitycabbage/engine/client/ui/ContainerLayout.java b/src/main/java/com/terminalvelocitycabbage/engine/client/ui/ContainerLayout.java new file mode 100644 index 0000000..fa29f6a --- /dev/null +++ b/src/main/java/com/terminalvelocitycabbage/engine/client/ui/ContainerLayout.java @@ -0,0 +1,144 @@ +package com.terminalvelocitycabbage.engine.client.ui; + +import org.joml.Matrix4f; +import org.joml.Vector3f; + +public class ContainerLayout extends Layout { + + Anchor anchor; + PlacementDirection placementDirection; + JustifyChildren justifyChildren; + + Matrix4f transformationMatrix; + + public ContainerLayout(Dimension width, Dimension height, Anchor anchor, PlacementDirection placementDirection, JustifyChildren justifyChildren) { + super(width, height); + this.anchor = anchor; + this.placementDirection = placementDirection; + this.justifyChildren = justifyChildren; + } + + public ContainerLayout(Dimension width, Dimension height) { + this(width, height, Anchor.CENTER_CENTER, PlacementDirection.CENTERED, JustifyChildren.CENTER); + } + + public ContainerLayout(int width, int height) { + this(new Dimension(width, Unit.PIXELS), new Dimension(height, Unit.PIXELS)); + } + + @Override + public void setDimensions(int width, int height) { + super.setDimensions(width, height); + this.computedWidth = width; + this.computedHeight = height; + } + + public enum Anchor { + TOP_LEFT(1, -1), + TOP_CENTER(1, 0), + TOP_RIGHT(1, 1), + CENTER_LEFT(0, -1), + CENTER_CENTER(0, 0), + CENTER_RIGHT(0, 1), + BOTTOM_LEFT( -1, -1), + BOTTOM_CENTER( -1, 0), + BOTTOM_RIGHT( -1, 1); + + public int verticalMultiplier; + public int horizontalMultiplier; + + Anchor(int verticalMultiplier, int horizontalMultiplier) { + this.verticalMultiplier = verticalMultiplier; + this.horizontalMultiplier = horizontalMultiplier; + } + } + + public enum PlacementDirection { + DOWN_RIGHT(-0.5f, 0.5f), + DOWN(-0.5f, 0), + DOWN_LEFT(-0.5f, -0.5f), + RIGHT(0, 0.5f), + CENTERED(0, 0), + LEFT(0, -0.5f), + UP_RIGHT(0.5f, 0.5f), + UP(0.5f, 0), + UP_LEFT(0.5f, -0.5f); + + float xMultiplier; + float yMultiplier; + + PlacementDirection(float yOffset, float xOffset) { + this.xMultiplier = xOffset; + this.yMultiplier = yOffset; + } + } + + public enum JustifyChildren { + TOP_LEFT(0.5f, -0.5f, PlacementDirection.DOWN_RIGHT), + TOP(0.5f, 0, PlacementDirection.DOWN), + TOP_RIGHT(0.5f, 0.5f, PlacementDirection.DOWN_LEFT), + LEFT(0, -0.5f, PlacementDirection.RIGHT), + CENTER(0, 0, PlacementDirection.CENTERED), + RIGHT(0, 0.5f, PlacementDirection.LEFT), + BOTTOM_LEFT( -0.5f, -0.5f, PlacementDirection.UP_RIGHT), + BOTTOM( -0.5f, 0, PlacementDirection.UP), + BOTTOM_RIGHT( -0.5f, 0.5f, PlacementDirection.UP_LEFT); + + public float verticalMultiplier; + public float horizontalMultiplier; + public PlacementDirection placementDirection; + + JustifyChildren(float verticalMultiplier, float horizontalMultiplier, PlacementDirection placementDirection) { + this.verticalMultiplier = verticalMultiplier; + this.horizontalMultiplier = horizontalMultiplier; + this.placementDirection = placementDirection; + } + } + + @Override + public Matrix4f getTransformationMatrix(ContainerLayout currentContainerLayout, Layout previousElementLayout) { + + transformationMatrix = new Matrix4f(); + + var pixelWidth = width.toPixelDimension(currentContainerLayout, true); + var pixelHeight = height.toPixelDimension(currentContainerLayout, false); + + var containerPixelWidth = currentContainerLayout.getComputedWidth(); + var containerPixelHeight = currentContainerLayout.getComputedHeight(); + + //Scale the object by its sizes + transformationMatrix.scale(pixelWidth, pixelHeight, 1); + //If the placement direction is not center, we want to move by half each dimension in the placement directions + //This is so that once we scale it'll already be in the right place + transformationMatrix.translateLocal( + placementDirection.xMultiplier * pixelWidth, + placementDirection.yMultiplier * pixelHeight, + 0); + + //Move the element to its proper location + var parentTransformationMatrix = currentContainerLayout.getStoredTransformationMatrix(); + transformationMatrix.translateLocal( + anchor.horizontalMultiplier * ((float) containerPixelWidth / 2), + anchor.verticalMultiplier * ((float) containerPixelHeight / 2), + 0); + //TODO precompute this somehow because it currently results in a one frame delay of proper layout generation + if (parentTransformationMatrix != null) transformationMatrix.translateLocal(parentTransformationMatrix.getTranslation(new Vector3f())); + return transformationMatrix; + } + + public PlacementDirection getPlacementDirection() { + return placementDirection; + } + + public Anchor getAnchor() { + return anchor; + } + + public JustifyChildren getChildJustification() { + return justifyChildren; + } + + public Matrix4f getStoredTransformationMatrix() { + return transformationMatrix; + } +} diff --git a/src/main/java/com/terminalvelocitycabbage/engine/client/ui/Element.java b/src/main/java/com/terminalvelocitycabbage/engine/client/ui/Element.java new file mode 100644 index 0000000..47aedab --- /dev/null +++ b/src/main/java/com/terminalvelocitycabbage/engine/client/ui/Element.java @@ -0,0 +1,45 @@ +package com.terminalvelocitycabbage.engine.client.ui; + +import com.terminalvelocitycabbage.engine.registry.Identifier; + +public final class Element { + + private Identifier parent; + private Layout layout; + private Style style; + + public Element(Identifier parent, Layout layout, Style style) { + this.parent = parent; + this.layout = layout; + this.style = style; + } + + public void setParent(Identifier parent) { + this.parent = parent; + } + + public Identifier getParent() { + return parent; + } + + public Layout getLayout() { + return layout; + } + + public Style getStyle() { + return style; + } + + public void reset() { + layout.reset(); + } + + @Override + public String toString() { + return "Element{" + + "parent=" + parent + + ", layout=" + layout.toString() + + ", style=" + style.toString() + + '}'; + } +} diff --git a/src/main/java/com/terminalvelocitycabbage/engine/client/ui/Font.java b/src/main/java/com/terminalvelocitycabbage/engine/client/ui/Font.java new file mode 100644 index 0000000..625cac5 --- /dev/null +++ b/src/main/java/com/terminalvelocitycabbage/engine/client/ui/Font.java @@ -0,0 +1,147 @@ +package com.terminalvelocitycabbage.engine.client.ui; + +import com.terminalvelocitycabbage.engine.debug.Log; +import com.terminalvelocitycabbage.engine.filesystem.resources.Resource; +import org.lwjgl.PointerBuffer; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.util.freetype.FT_Face; +import org.lwjgl.util.freetype.FreeType; +import org.lwjgl.util.harfbuzz.HarfBuzz; +import org.lwjgl.util.harfbuzz.hb_glyph_info_t; +import org.lwjgl.util.harfbuzz.hb_glyph_position_t; + +import java.util.stream.IntStream; + +public class Font { + + private final int fontSize; //In pixels + private final long freeType; + private final long facePointer; + private final FT_Face face; + private final long font; + + public static final int[] ISO_8859_1_GLYPH_IDS = IntStream.rangeClosed(32, 126).toArray(); + + public Font(Resource resource, int fontSize) { + + this.fontSize = fontSize; + + //Init freetype for font loading + try (MemoryStack stack = MemoryStack.stackPush()) { + //Init freetype and store pointer to library for later + PointerBuffer ftPointer = stack.callocPointer(1); + FreeType.FT_Init_FreeType(ftPointer); + freeType = ftPointer.get(0); + + //Load font + PointerBuffer facePointerBuffer = stack.callocPointer(1); + FreeType.FT_New_Memory_Face(freeType, resource.asByteBuffer(true), 0, facePointerBuffer); + facePointer = facePointerBuffer.get(0); + + //Configure font + face = FT_Face.create(this.facePointer); + FreeType.FT_Set_Pixel_Sizes(face, 0, fontSize); + } + + //Init harfbuzz + font = HarfBuzz.hb_ft_font_create_referenced(facePointer); + } + + public ShapedText shapeText(String text) { + try (MemoryStack stack = MemoryStack.stackPush()) { + long buffer = HarfBuzz.hb_buffer_create(); + HarfBuzz.hb_buffer_add_utf8(buffer, text, 0, text.length()); + HarfBuzz.hb_buffer_guess_segment_properties(buffer); + + HarfBuzz.hb_shape(font, buffer, null); + + int glyphCount = HarfBuzz.hb_buffer_get_length(buffer); + hb_glyph_info_t.Buffer infos = HarfBuzz.hb_buffer_get_glyph_infos(buffer); + hb_glyph_position_t.Buffer positions = HarfBuzz.hb_buffer_get_glyph_positions(buffer); + + ShapedText shaped = new ShapedText(glyphCount); + for (int i = 0; i < glyphCount; i++) { + int glyphId = infos.get(i).codepoint(); + float xAdvance = positions.get(i).x_advance() / 64f; + float yAdvance = positions.get(i).y_advance() / 64f; + float xOffset = positions.get(i).x_offset() / 64f; + float yOffset = positions.get(i).y_offset() / 64f; + shaped.setGlyph(i, glyphId, xAdvance, yAdvance, xOffset, yOffset); + } + + HarfBuzz.hb_buffer_destroy(buffer); + return shaped; + } + } + + public void drawText(ShapedText text, float startX, float startY) { + + float x = startX; + float y = startY; + + for (int i = 0; i < text.glyphCount; i++) { + GlyphData glyphData = text.glyphs[i]; + + //TODO Draw + Log.info("Drawing glyph " + glyphData.glyphId + " at " + x + glyphData.xOffset + ", " + y + glyphData.yOffset); + + x += glyphData.xAdvance; + y += glyphData.yAdvance; + } + + } + + public void cleanup() { + HarfBuzz.hb_font_destroy(font); + FreeType.FT_Done_Face(face); + FreeType.FT_Done_FreeType(freeType); + } + + public static class ShapedText { + final int glyphCount; + final GlyphData[] glyphs; + + ShapedText(int glyphCount) { + this.glyphCount = glyphCount; + this.glyphs = new GlyphData[glyphCount]; + } + + void setGlyph(int index, int glyphId, float xAdv, float yAdv, float xOff, float yOff) { + glyphs[index] = new GlyphData(glyphId, xAdv, yAdv, xOff, yOff); + } + } + + public static class GlyphData { + final int glyphId; + final float xAdvance, yAdvance; + final float xOffset, yOffset; + + GlyphData(int glyphId, float xAdvance, float yAdvance, float xOffset, float yOffset) { + this.glyphId = glyphId; + this.xAdvance = xAdvance; + this.yAdvance = yAdvance; + this.xOffset = xOffset; + this.yOffset = yOffset; + } + } + + public int getFontSize() { + return fontSize; + } + + public long getFreeType() { + return freeType; + } + + public long getFacePointer() { + return facePointer; + } + + public FT_Face getFace() { + return face; + } + + public long getFont() { + return font; + } +} diff --git a/src/main/java/com/terminalvelocitycabbage/engine/client/ui/Layout.java b/src/main/java/com/terminalvelocitycabbage/engine/client/ui/Layout.java new file mode 100644 index 0000000..a0c86a7 --- /dev/null +++ b/src/main/java/com/terminalvelocitycabbage/engine/client/ui/Layout.java @@ -0,0 +1,126 @@ +package com.terminalvelocitycabbage.engine.client.ui; + +import com.terminalvelocitycabbage.engine.debug.Log; +import org.joml.Matrix4f; +import org.joml.Vector3f; + +public class Layout { + + Dimension width; + Dimension height; + + //In Pixels + int computedWidth; + int computedHeight; + + int nthChild = 0; + + public Layout(Dimension width, Dimension height) { + this.width = width; + this.height = height; + } + + public Layout(int width, int height) { + this(new Dimension(width, Unit.PIXELS), new Dimension(height, Unit.PIXELS)); + } + + public void computeDimensions(Layout parentLayout) { + computedWidth = (int) (width.toPixelDimension(parentLayout, true)); + computedHeight = (int) (height.toPixelDimension(parentLayout, false)); + } + + public void setDimensions(int width, int height) { + this.width = new Dimension(width, Unit.PIXELS); + this.height = new Dimension(height, Unit.PIXELS); + } + + public enum Unit { + PIXELS, + PERCENT; + } + + public record Dimension(Integer value, Unit unit) { + + float toPixelDimension(Layout parentLayout, boolean width) { + if (unit == Unit.PIXELS) return value; + if (width) { + return parentLayout.getComputedWidth() * ((float) value / 100); + } else { + return parentLayout.getComputedHeight() * ((float) value / 100); + } + } + + @Override + public String toString() { + return "Dimension{" + + "value=" + value + + ", unit=" + unit + + '}'; + } + } + + public Matrix4f getTransformationMatrix(ContainerLayout currentContainerLayout, Layout previousElementLayout) { + + Matrix4f transformationMatrix = new Matrix4f(); + + var pixelWidth = width.toPixelDimension(currentContainerLayout, true); + var pixelHeight = height.toPixelDimension(currentContainerLayout, false); + + var containerPixelWidth = currentContainerLayout.getComputedWidth(); + var containerPixelHeight = currentContainerLayout.getComputedHeight(); + + //Scale the object by its sizes + transformationMatrix.scale(pixelWidth, pixelHeight, 1); + + //Move the element to its proper location + //Locate the elements based on container + var parentTransformationMatrix = currentContainerLayout.getStoredTransformationMatrix(); + if (parentTransformationMatrix != null) transformationMatrix.translateLocal(parentTransformationMatrix.getTranslation(new Vector3f())); + //Justify element based on container + transformationMatrix.translateLocal( + containerPixelWidth * currentContainerLayout.getChildJustification().horizontalMultiplier, + containerPixelHeight * currentContainerLayout.getChildJustification().verticalMultiplier, + 0); + transformationMatrix.translateLocal( + pixelWidth * currentContainerLayout.getChildJustification().placementDirection.xMultiplier, + pixelHeight * currentContainerLayout.getChildJustification().placementDirection.yMultiplier, + 0 + ); + //Place relative to previous sibling + //TODO do container children positions here (don't move x and y, use container justify children to decide how to offset) + if (previousElementLayout != null) { + Log.info("Previous element stuff: " + previousElementLayout.getComputedWidth() + " x " + previousElementLayout.getComputedHeight()); + transformationMatrix.translateLocal( + previousElementLayout.getComputedWidth() * currentContainerLayout.getChildJustification().placementDirection.xMultiplier * 2 * nthChild, + previousElementLayout.getComputedHeight() * currentContainerLayout.getChildJustification().placementDirection.yMultiplier * 2 * nthChild, + 0 + ); + } + + nthChild++; + + Log.info("Nth Child incremented to: " + nthChild); + + return transformationMatrix; + } + + public Dimension getHeight() { + return height; + } + + public Dimension getWidth() { + return width; + } + + public int getComputedWidth() { + return computedWidth; + } + + public int getComputedHeight() { + return computedHeight; + } + + public void reset() { + nthChild = 0; + } +} diff --git a/src/main/java/com/terminalvelocitycabbage/engine/client/ui/Style.java b/src/main/java/com/terminalvelocitycabbage/engine/client/ui/Style.java new file mode 100644 index 0000000..4a1aba3 --- /dev/null +++ b/src/main/java/com/terminalvelocitycabbage/engine/client/ui/Style.java @@ -0,0 +1,58 @@ +package com.terminalvelocitycabbage.engine.client.ui; + +import com.terminalvelocitycabbage.engine.registry.Identifier; + +public class Style { + + Identifier textureIdentifier; + Identifier fontIdentifier; + + public Style(Identifier textureIdentifier, Identifier fontIdentifier) { + this.textureIdentifier = textureIdentifier; + this.fontIdentifier = fontIdentifier; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private Identifier textureIdentifier; + private Identifier fontIdentifier; + + public Builder() { + this.textureIdentifier = null; + this.fontIdentifier = null; + } + + public Builder setTexture(Identifier textureIdentifier) { + this.textureIdentifier = textureIdentifier; + return this; + } + + public Builder setFont(Identifier fontIdentifier) { + this.fontIdentifier = fontIdentifier; + return this; + } + + public Style build() { + return new Style(textureIdentifier, fontIdentifier); + } + } + + public Identifier getTextureIdentifier() { + return textureIdentifier; + } + + public Identifier getFontIdentifier() { + return fontIdentifier; + } + + @Override + public String toString() { + return "Style{" + + "textureIdentifier=" + textureIdentifier + + ", fontIdentifier=" + fontIdentifier + + '}'; + } +} diff --git a/src/main/java/com/terminalvelocitycabbage/engine/client/ui/UIContext.java b/src/main/java/com/terminalvelocitycabbage/engine/client/ui/UIContext.java new file mode 100644 index 0000000..584f9b0 --- /dev/null +++ b/src/main/java/com/terminalvelocitycabbage/engine/client/ui/UIContext.java @@ -0,0 +1,52 @@ +package com.terminalvelocitycabbage.engine.client.ui; + +import com.terminalvelocitycabbage.engine.client.window.WindowProperties; +import com.terminalvelocitycabbage.engine.registry.Identifier; + +public class UIContext { + + Identifier previousContainer; + Identifier currentContainer; + Identifier previousElement; + + WindowProperties windowProperties; + + public UIContext(WindowProperties windowProperties) { + previousContainer = null; + currentContainer = null; + previousElement = null; + this.windowProperties = windowProperties; + } + + public Identifier getPreviousContainer() { + return previousContainer; + } + + public void setPreviousContainer(Identifier previousContainer) { + this.previousContainer = previousContainer; + } + + public Identifier getCurrentContainer() { + return currentContainer; + } + + public void setCurrentContainer(Identifier currentContainer) { + this.currentContainer = currentContainer; + } + + public Identifier getPreviousElement() { + return previousElement; + } + + public void setPreviousElement(Identifier previousSibling) { + this.previousElement = previousSibling; + } + + public int getWindowWidth() { + return windowProperties.getWidth(); + } + + public int getWindowHeight() { + return windowProperties.getHeight(); + } +} diff --git a/src/main/java/com/terminalvelocitycabbage/engine/graph/UIRenderNode.java b/src/main/java/com/terminalvelocitycabbage/engine/graph/UIRenderNode.java new file mode 100644 index 0000000..7ea1aaf --- /dev/null +++ b/src/main/java/com/terminalvelocitycabbage/engine/graph/UIRenderNode.java @@ -0,0 +1,213 @@ +package com.terminalvelocitycabbage.engine.graph; + +import com.terminalvelocitycabbage.engine.client.ClientBase; +import com.terminalvelocitycabbage.engine.client.renderer.Projection; +import com.terminalvelocitycabbage.engine.client.renderer.RenderGraph; +import com.terminalvelocitycabbage.engine.client.renderer.elements.VertexAttribute; +import com.terminalvelocitycabbage.engine.client.renderer.elements.VertexFormat; +import com.terminalvelocitycabbage.engine.client.renderer.model.Mesh; +import com.terminalvelocitycabbage.engine.client.renderer.model.MeshCache; +import com.terminalvelocitycabbage.engine.client.renderer.model.Model; +import com.terminalvelocitycabbage.engine.client.renderer.shader.ShaderProgramConfig; +import com.terminalvelocitycabbage.engine.client.scene.Scene; +import com.terminalvelocitycabbage.engine.client.ui.*; +import com.terminalvelocitycabbage.engine.client.window.WindowProperties; +import com.terminalvelocitycabbage.engine.debug.Log; +import com.terminalvelocitycabbage.engine.registry.Identifier; +import com.terminalvelocitycabbage.engine.registry.Registry; +import com.terminalvelocitycabbage.engine.util.HeterogeneousMap; +import com.terminalvelocitycabbage.templates.meshes.SquareDataMesh; + +import static org.lwjgl.opengl.GL11.*; + +/* TODO +* It may be an issue of some reused elements being quasi connected. +* We need to replace the elementRegistry with some sort of element configuration registry that elements can be built +* from when they're used so that their actual identifiers in the render tree are not the same (when two identical +* element identifiers are used on a call they aren't modifying eachother). OR the other option is to make sure that +* in each of the drawX methods we don't modify any state for the current element before it is drawn to the screen +* since this is technically an immediate mode renderer it might be fine as is, it will depend mostly on how we handle +* saved states for more complex elements. I assume these will just be variables edited by actions not stored on any +* elements directly though so again, it might be fine. +*/ + +public abstract class UIRenderNode extends RenderNode { + + static final Identifier ROOT_ELEMENT_IDENTIFIER = ClientBase.getInstance().identifierOf("root"); + static final Projection PROJECTION = new Projection(Projection.Type.ORTHOGONAL, 0, 100); + MeshCache meshCache; + Registry elementRegistry; + + public static final VertexFormat UI_ELEMENT_MESH_FORMAT = VertexFormat.builder() + .addElement(VertexAttribute.XYZ_POSITION) + .addElement(VertexAttribute.RGB_COLOR) + .addElement(VertexAttribute.UV) + .build(); + static final Mesh QUAD_MESH = new Mesh(UI_ELEMENT_MESH_FORMAT, new SquareDataMesh()); + + UIContext context; + + public UIRenderNode(ShaderProgramConfig shaderProgramConfig) { + super(shaderProgramConfig); + } + + //TODO this stuff needs to be done for all ui nodes on a graph, we don't really want to create a new cache for every UI node I don't think + @Override + public void init(RenderGraph renderGraph) { + super.init(renderGraph); + + //Collect all elements used in this UI so that the textures and meshes can be cached + this.elementRegistry = new Registry<>(); + var rootElement = new Element(null, new ContainerLayout(new Layout.Dimension(0, Layout.Unit.PIXELS), new Layout.Dimension(0, Layout.Unit.PIXELS), ContainerLayout.Anchor.CENTER_CENTER, ContainerLayout.PlacementDirection.CENTERED, ContainerLayout.JustifyChildren.CENTER), Style.builder().build()); + elementRegistry.register(ROOT_ELEMENT_IDENTIFIER, rootElement); + registerUIElements(new ElementRegistry(elementRegistry)); + + //Create a mesh cache so that textures can be sampled from the UI atlas + Registry meshRegistry = new Registry<>(); + //All meshes that are provided by the UIRenderNode should be registered to the mesh registry here + var quadMesh = ClientBase.getInstance().identifierOf("quad"); + meshRegistry.register(quadMesh, QUAD_MESH); + //Pair up meshes with textures here based on all textures used in element styles + Registry modelRegistry = new Registry<>(); + elementRegistry.getRegistryContents().forEach((identifier, element) -> { + var texture = element.getStyle().getTextureIdentifier(); + //TODO this will eventually depend on what type of texture it is using, eventually we will have textures with unique edge, corner, and middle conditions + if (texture != null) modelRegistry.register(identifier, new Model(quadMesh, texture)); + }); + //Generate the mesh cache from the paired meshes and textures (models) + this.meshCache = new MeshCache(modelRegistry, meshRegistry, ClientBase.getInstance().getTextureCache()); + } + + public abstract void registerUIElements(ElementRegistry elementRegistry); + + public abstract void drawUIElements(Scene scene); + + @Override + public void execute(Scene scene, WindowProperties properties, HeterogeneousMap renderConfig, long deltaTime) { + + //Set the shader up for rendering + var shaderProgram = getShaderProgram(); + if (properties.isResized()) PROJECTION.updateProjectionMatrix(properties.getWidth(), properties.getHeight()); + shaderProgram.bind(); + shaderProgram.getUniform("textureSampler").setUniform(0); + shaderProgram.getUniform("projectionMatrix").setUniform(PROJECTION.getProjectionMatrix()); + + elementRegistry.get(ROOT_ELEMENT_IDENTIFIER).getLayout().setDimensions(properties.getWidth(), properties.getHeight()); + context = new UIContext(properties); + context.setPreviousContainer(null); + context.setPreviousElement(null); + context.setCurrentContainer(ROOT_ELEMENT_IDENTIFIER); + + Log.info("Rendering UI -------------------------"); + glClear(GL_DEPTH_BUFFER_BIT); + drawUIElements(scene); + + //Reset + shaderProgram.unbind(); + + elementRegistry.getRegistryContents().values().forEach(Element::reset); + } + + //TODO add some warnings if a container is started but not ended as it creates some weird things if you don't + public boolean startContainer(Identifier elementIdentifier) { + + Log.info(elementIdentifier + " started"); + + context.setPreviousElement(null); + context.setPreviousContainer(context.getCurrentContainer()); + var current = elementRegistry.get(elementIdentifier); + current.setParent(context.getCurrentContainer()); + current.getLayout().computeDimensions(elementRegistry.get(context.getCurrentContainer()).getLayout()); + context.setCurrentContainer(elementIdentifier); + + return true; + } + + public void endContainer() { + + var thisElement = elementRegistry.get(context.getCurrentContainer()); + var thisLayout = (ContainerLayout) thisElement.getLayout(); + var style = thisElement.getStyle(); + + if (style.getTextureIdentifier() != null) { + ClientBase.getInstance().getTextureCache().getTexture(style.getTextureIdentifier()).bind(); + } + + var previousParentContainer = elementRegistry.get(context.getPreviousContainer()).getParent(); + shaderProgram.getUniform("modelMatrix").setUniform( + thisLayout.getTransformationMatrix( + (ContainerLayout) elementRegistry.get(context.getPreviousContainer()).getLayout(), + context.getPreviousElement() == null ? null : elementRegistry.get(context.getPreviousElement()).getLayout() + ) + ); + + meshCache.getMesh(context.getCurrentContainer()).render(); + + Log.info(context.getCurrentContainer() + " ended"); + + context.setPreviousElement(context.getCurrentContainer()); + context.setCurrentContainer(context.getPreviousContainer()); + context.setPreviousContainer(previousParentContainer == null ? context.getPreviousContainer() : previousParentContainer); + + elementRegistry.getRegistryContents().values().forEach(Element::reset); + + } + + public boolean drawText(Identifier elementIdentifier, String text) { + + Element thisElement = elementRegistry.get(elementIdentifier); + Log.info("Drawing text " + text + " with element " + thisElement); + Identifier fontIdentifier = thisElement.getStyle().getFontIdentifier(); + Log.info("Drawing text " + text + " with style " + thisElement.getStyle()); + Font font = ClientBase.getInstance().getFontRegistry().get(fontIdentifier); + + Font.ShapedText shapedText = font.shapeText(text); + + font.drawText(shapedText, 0, 0); + + return true; + } + + public boolean drawBox(Identifier elementIdentifier) { + + var thisElement = elementRegistry.get(elementIdentifier); + var layout = thisElement.getLayout(); + var style = thisElement.getStyle(); + + if (style.getTextureIdentifier() != null) { + var texture = ClientBase.getInstance().getTextureCache().getTexture(style.getTextureIdentifier()); + texture.bind(); + } + + shaderProgram.getUniform("modelMatrix").setUniform( + layout.getTransformationMatrix( + (ContainerLayout) elementRegistry.get(context.getCurrentContainer()).getLayout(), + context.getPreviousElement() == null ? null : elementRegistry.get(context.getPreviousElement()).getLayout() + ) + ); + + meshCache.getMesh(elementIdentifier).render(); + + layout.computeDimensions(elementRegistry.get(context.getCurrentContainer()).getLayout()); + + Log.info(elementIdentifier + " drawn"); + + context.setPreviousElement(elementIdentifier); + + return true; + } + + public static class ElementRegistry extends Registry { + + Registry elementRegistry; + + public ElementRegistry(Registry elementRegistry) { + this.elementRegistry = elementRegistry; + } + + public Identifier registerElement(Identifier elementID, Layout layout, Style style) { + return elementRegistry.register(elementID, new Element(null, layout, style)).getIdentifier(); + } + } + +} diff --git a/src/main/java/com/terminalvelocitycabbage/engine/registry/Registry.java b/src/main/java/com/terminalvelocitycabbage/engine/registry/Registry.java index 3641ba7..d555777 100644 --- a/src/main/java/com/terminalvelocitycabbage/engine/registry/Registry.java +++ b/src/main/java/com/terminalvelocitycabbage/engine/registry/Registry.java @@ -33,7 +33,7 @@ public Registry() { */ public RegistryPair register(Identifier identifier, T item) { if (registryContents.containsKey(identifier)) { - Log.warn("Tried to register item of same identifier " + identifier.toString() + " twice, the second addition has been ignored."); + Log.warn("Tried to register item of same identifier " + identifier.toString() + " twice, the second addition has been ignored. This will likely cause issues later on (probably crashes)."); return null; } registryContents.put(identifier, item); diff --git a/src/main/java/com/terminalvelocitycabbage/engine/util/BufferUtils.java b/src/main/java/com/terminalvelocitycabbage/engine/util/BufferUtils.java new file mode 100644 index 0000000..f676a3e --- /dev/null +++ b/src/main/java/com/terminalvelocitycabbage/engine/util/BufferUtils.java @@ -0,0 +1,42 @@ +package com.terminalvelocitycabbage.engine.util; + +import org.joml.Vector2i; +import org.lwjgl.system.MemoryUtil; + +import java.nio.ByteBuffer; + +public class BufferUtils { + + /** + * Inserts the subimage into this atlas buffer + * @param atlas The atlas image buffer which this image is being added to + * @param subImage The image buffer to be inserted + * @param dest The location for the sub image buffer + */ + public static void insertSubImage(ByteBuffer atlas, int atlasW, int atlasH, ByteBuffer subImage, int subImageW, int subImageH, Vector2i dest) { + + long atlasBaseAddr = MemoryUtil.memAddress(atlas); + long subImageBaseAddr = MemoryUtil.memAddress(subImage); + + for (int row = 0; row < subImageH; row++) { + int destRow = dest.y() + row; + if (destRow >= atlasH || dest.x() + subImageW > atlasW) continue; + + long destOffset = ((long) destRow * atlasW + dest.x()) * 4; + long srcOffset = ((long) row * subImageW) * 4; + + MemoryUtil.memCopy( + subImageBaseAddr + srcOffset, + atlasBaseAddr + destOffset, + subImageW * 4L + ); + } + } + + public static Vector2i insertSubImage(ByteBuffer atlas, int atlasW, int atlasH, ByteBuffer subImage, int subImageW, int subImageH) { + Vector2i location = new Vector2i(); + insertSubImage(atlas, atlasW, atlasH, subImage, subImageW, subImageH, location); + return location; + } + +} diff --git a/src/main/java/com/terminalvelocitycabbage/engine/util/MathUtils.java b/src/main/java/com/terminalvelocitycabbage/engine/util/MathUtils.java index 89b02b2..cb0ad22 100644 --- a/src/main/java/com/terminalvelocitycabbage/engine/util/MathUtils.java +++ b/src/main/java/com/terminalvelocitycabbage/engine/util/MathUtils.java @@ -63,4 +63,12 @@ public static Vector2i findMostSquareDimensions(int minArea) { return new Vector2i(bestWidth, bestHeight); } + public static int findNearestPowerOfTwo(int number) { + int powerOfTwo = 1; + while (powerOfTwo < number) { + powerOfTwo <<= 1; + } + return powerOfTwo; + } + } diff --git a/src/main/java/com/terminalvelocitycabbage/engine/util/Transformation.java b/src/main/java/com/terminalvelocitycabbage/engine/util/Transformation.java index b665f35..ab19ca2 100644 --- a/src/main/java/com/terminalvelocitycabbage/engine/util/Transformation.java +++ b/src/main/java/com/terminalvelocitycabbage/engine/util/Transformation.java @@ -72,6 +72,24 @@ public Transformation setScale(float scale) { return this; } + public Transformation setScale(float x, float y, float z) { + this.scale.set(x, y, z); + dirty = true; + return this; + } + + public Transformation addScale(float x, float y, float z) { + this.scale.add(x, y, z); + this.dirty = true; + return this; + } + + public Transformation addScale(Vector3f scale) { + this.scale.add(scale); + this.dirty = true; + return this; + } + public Matrix4f getTransformationMatrix() { if (dirty) updateTransformationMatrix(); return transformationMatrix; diff --git a/src/main/java/com/terminalvelocitycabbage/templates/events/ConfigureTexturesEvent.java b/src/main/java/com/terminalvelocitycabbage/templates/events/ConfigureTexturesEvent.java index 48eda77..ac3ed2f 100644 --- a/src/main/java/com/terminalvelocitycabbage/templates/events/ConfigureTexturesEvent.java +++ b/src/main/java/com/terminalvelocitycabbage/templates/events/ConfigureTexturesEvent.java @@ -1,6 +1,7 @@ package com.terminalvelocitycabbage.templates.events; import com.terminalvelocitycabbage.engine.TerminalVelocityEngine; +import com.terminalvelocitycabbage.engine.debug.Log; import com.terminalvelocitycabbage.engine.event.Event; import com.terminalvelocitycabbage.engine.filesystem.GameFileSystem; import com.terminalvelocitycabbage.engine.filesystem.resources.Resource; @@ -36,6 +37,7 @@ public void addTexture(Identifier textureIdentifier, Identifier... atlasIdentifi singleTextures.putIfAbsent(textureIdentifier, fileSystem.getResource(ResourceCategory.TEXTURE, textureIdentifier)); } else { for (Identifier atlasIdentifier : atlasIdentifiers) { + if (!texturesToCompileToAtlas.containsKey(atlasIdentifier)) Log.crash("Tried to add texture to non-existent atlas, make sure that the atlas was registered beforehand"); texturesToCompileToAtlas.get(atlasIdentifier).putIfAbsent(textureIdentifier, fileSystem.getResource(ResourceCategory.TEXTURE, textureIdentifier)); } } diff --git a/src/main/java/com/terminalvelocitycabbage/templates/events/GenerateFontsEvent.java b/src/main/java/com/terminalvelocitycabbage/templates/events/GenerateFontsEvent.java new file mode 100644 index 0000000..b79c0fe --- /dev/null +++ b/src/main/java/com/terminalvelocitycabbage/templates/events/GenerateFontsEvent.java @@ -0,0 +1,33 @@ +package com.terminalvelocitycabbage.templates.events; + +import com.terminalvelocitycabbage.engine.TerminalVelocityEngine; +import com.terminalvelocitycabbage.engine.client.ui.Font; +import com.terminalvelocitycabbage.engine.event.Event; +import com.terminalvelocitycabbage.engine.filesystem.GameFileSystem; +import com.terminalvelocitycabbage.engine.filesystem.resources.ResourceCategory; +import com.terminalvelocitycabbage.engine.registry.Identifier; +import com.terminalvelocitycabbage.engine.registry.Registry; +import org.lwjgl.system.Configuration; +import org.lwjgl.util.freetype.FreeType; + +public class GenerateFontsEvent extends Event { + + public static final Identifier EVENT = TerminalVelocityEngine.identifierOf("GenerateFontsEvent"); + + private GameFileSystem fileSystem; + private Registry fontRegistry; + + public GenerateFontsEvent(GameFileSystem fileSystem, Registry fontRegistry) { + super(EVENT); + this.fileSystem = fileSystem; + this.fontRegistry = fontRegistry; + Configuration.HARFBUZZ_LIBRARY_NAME.set(FreeType.getLibrary()); + } + + public Identifier generateFont(Identifier fontResourceIdentifier, int fontSize) { + var resource = fileSystem.getResource(ResourceCategory.FONT, fontResourceIdentifier); + var fontIdentifier = new Identifier(fontResourceIdentifier.getNamespace(), fontResourceIdentifier.getName() + "_" + fontSize); + fontRegistry.register(fontIdentifier, new Font(resource, fontSize)); + return fontIdentifier; + } +}