Initial font renderer rewrite and Libra integration

This commit is contained in:
Natty 2022-04-12 01:08:57 +02:00
parent c675729996
commit 7b7daafc15
No known key found for this signature in database
GPG Key ID: 40AB22FA416C7019
58 changed files with 1662 additions and 832 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "libra"]
path = libra
url = git@github.com:493msi/libra.git

View File

@ -1,3 +1,10 @@
## 22.1.0.0-alpha.0
* `[PlutoGUI]` **Complete rewrite of the GUI library**
* `[Pluto*]` **Unified the cleanup methods of all OpenGL object classes to `close`**
* `[PlutoLib]` New dependency: `joml-primitives`
* `[PlutoDisplay]` Removed the `flipped` word from all buffer functions
* `[PlutoRuntime]` Fixed opening .zip filesystems
## 22.0.0.0-alpha.7 ## 22.0.0.0-alpha.7
* `[PlutoRuntime]` Fixed several resource filesystem bugs * `[PlutoRuntime]` Fixed several resource filesystem bugs

View File

@ -12,17 +12,18 @@ object Versions {
} }
const val jomlVersion = "1.10.2" const val jomlVersion = "1.10.2"
const val jomlPrimitivesVersion = "1.10.0"
const val steamworks4jVersion = "1.8.0" const val steamworks4jVersion = "1.8.0"
const val steamworks4jServerVersion = "1.8.0" const val steamworks4jServerVersion = "1.8.0"
const val versionYear = 22 const val versionYear = 22
const val versionMajor = 0 const val versionMajor = 1
const val versionMinor = 0 const val versionMinor = 0
const val versionPatch = 0 const val versionPatch = 0
const val isPrerelease = true const val isPrerelease = true
const val prereleaseName = "alpha" const val prereleaseName = "alpha"
const val prerealeaseUpdate = 7 const val prerealeaseUpdate = 0
val versionFull = val versionFull =
if (isPrerelease) if (isPrerelease)

View File

@ -3,7 +3,7 @@ package org.plutoengine.audio.al;
import org.joml.Vector3fc; import org.joml.Vector3fc;
import org.lwjgl.openal.AL10; import org.lwjgl.openal.AL10;
public abstract class AudioSource public abstract class AudioSource implements AutoCloseable
{ {
protected final int source; protected final int source;

View File

@ -28,7 +28,7 @@ public final class BufferHelper
* @author 493msi * @author 493msi
* @since 0.1 * @since 0.1
*/ */
public static IntBuffer flippedIntBuffer(int[] data) public static IntBuffer intBuffer(int[] data)
{ {
IntBuffer buffer = BufferUtils.createIntBuffer(data.length); IntBuffer buffer = BufferUtils.createIntBuffer(data.length);
buffer.put(data); buffer.put(data);
@ -46,7 +46,7 @@ public final class BufferHelper
* @author 493msi * @author 493msi
* @since 0.1 * @since 0.1
*/ */
public static FloatBuffer flippedFloatBuffer(float[] data) public static FloatBuffer floatBuffer(float[] data)
{ {
FloatBuffer buffer = BufferUtils.createFloatBuffer(data.length); FloatBuffer buffer = BufferUtils.createFloatBuffer(data.length);
buffer.put(data); buffer.put(data);
@ -64,7 +64,7 @@ public final class BufferHelper
* @author 493msi * @author 493msi
* @since 0.1 * @since 0.1
*/ */
public static ByteBuffer flippedByteBuffer(byte[] data) public static ByteBuffer byteBuffer(byte[] data)
{ {
ByteBuffer buffer = BufferUtils.createByteBuffer(data.length); ByteBuffer buffer = BufferUtils.createByteBuffer(data.length);
buffer.put(data); buffer.put(data);
@ -136,8 +136,8 @@ public final class BufferHelper
* @author 493msi * @author 493msi
* @since 0.3 * @since 0.3
*/ */
public static ByteBuffer readToFlippedByteBuffer(Path path) throws IOException public static ByteBuffer readToByteBuffer(Path path) throws IOException
{ {
return flippedByteBuffer(Files.readAllBytes(path)); return byteBuffer(Files.readAllBytes(path));
} }
} }

View File

@ -7,4 +7,12 @@ description = ""
dependencies { dependencies {
api(project(":plutoengine:plutospritesheet")) api(project(":plutoengine:plutospritesheet"))
api(project(":libra"))
api("io.reactivex.rxjava3", "rxjava", "3.1.4")
implementation("org.lwjgl", "lwjgl-yoga")
runtimeOnly("org.lwjgl", "lwjgl-yoga", classifier = org.plutoengine.Versions.lwjglNatives)
implementation("org.commonmark", "commonmark", "0.18.1")
} }

View File

@ -1,13 +1,12 @@
package org.plutoengine.graphics; package org.plutoengine.graphics;
import org.joml.Matrix3f;
import org.plutoengine.Pluto; import org.plutoengine.Pluto;
import org.plutoengine.graphics.font.FontManager; import org.plutoengine.graphics.gui.FontShader;
import org.plutoengine.graphics.font.FontShader;
import org.plutoengine.graphics.texture.MagFilter; import org.plutoengine.graphics.texture.MagFilter;
import org.plutoengine.graphics.texture.MinFilter; import org.plutoengine.graphics.texture.MinFilter;
import org.plutoengine.graphics.texture.WrapMode; import org.plutoengine.graphics.texture.WrapMode;
import org.plutoengine.graphics.texture.texture2d.RectangleTexture; import org.plutoengine.graphics.texture.texture2d.RectangleTexture;
import org.plutoengine.gui.font.FontRenderer;
import org.plutoengine.mod.IModEntryPoint; import org.plutoengine.mod.IModEntryPoint;
import org.plutoengine.mod.Mod; import org.plutoengine.mod.Mod;
import org.plutoengine.mod.ModEntry; import org.plutoengine.mod.ModEntry;
@ -28,18 +27,17 @@ public class PlutoGUIMod implements IModEntryPoint
public static RectangleTexture uiElementsAtlas; public static RectangleTexture uiElementsAtlas;
private static FontShader fontShader; public static FontShader fontShader;
public void onLoad(Mod mod) public void onLoad(Mod mod)
{ {
instance = mod; instance = mod;
fontShader = new RenderShaderBuilder(mod.getResource("shaders.VertexFontShader#glsl"), mod.getResource("shaders.FragmentFontShader#glsl")).build(FontShader.class, false); fontShader = new RenderShaderBuilder(mod.getResource("shaders.VertexFontShader#glsl"), mod.getResource("shaders.FragmentFontShader#glsl")).build(FontShader.class, false);
fontShader.start();
fontShader.recolor.load(1, 1, 1, 1);
fontShader.transformationMatrix.load(new Matrix3f());
// Load the default font
FontManager.loadFont(mod.getResource("font.default"));
FontRenderer.load(fontShader);
uiElementsAtlas = new RectangleTexture(); uiElementsAtlas = new RectangleTexture();
uiElementsAtlas.load(mod.getResource("gui.elements#png"), MagFilter.NEAREST, MinFilter.NEAREST, WrapMode.CLAMP_TO_EDGE, WrapMode.CLAMP_TO_EDGE); uiElementsAtlas.load(mod.getResource("gui.elements#png"), MagFilter.NEAREST, MinFilter.NEAREST, WrapMode.CLAMP_TO_EDGE, WrapMode.CLAMP_TO_EDGE);
@ -47,11 +45,8 @@ public class PlutoGUIMod implements IModEntryPoint
public void onUnload() public void onUnload()
{ {
uiElementsAtlas.delete(); uiElementsAtlas.close();
FontManager.unloadAll(); fontShader.close();
FontRenderer.unload();
fontShader.dispose();
} }
} }

View File

@ -0,0 +1,33 @@
package org.plutoengine.graphics;
import org.joml.Matrix3f;
import org.plutoengine.graphics.gui.PlutoGUICommandParser;
import org.plutoengine.graphics.gui.STBTTBasicTextShaper;
import org.plutoengine.graphics.gui.STBTTFont;
import org.plutoengine.libra.command.LiCommandBuffer;
import org.plutoengine.libra.command.impl.LiCommandSetTransform;
import org.plutoengine.libra.text.shaping.TextShaper;
import java.util.EnumSet;
public class TestFontRenderer
{
public static void drawString(STBTTFont font, String text)
{
var shaper = new STBTTBasicTextShaper();
var info = shaper.shape(EnumSet.of(TextShaper.EnumFeature.KERNING), font, text);
var transformBuf = new LiCommandBuffer();
transformBuf.push(new LiCommandSetTransform(new Matrix3f().scale(.75f).m20(400f).m21(400f)));
var buf = info.getDrawCommandBuffer();
var commandParser = new PlutoGUICommandParser();
commandParser.add(transformBuf);
commandParser.add(buf);
var drawCalls = commandParser.parse();
drawCalls.render();
drawCalls.close();
}
}

View File

@ -1,96 +0,0 @@
package org.plutoengine.graphics.font;
import org.plutoengine.graphics.texture.MagFilter;
import org.plutoengine.graphics.texture.MinFilter;
import org.plutoengine.graphics.texture.Texture;
import org.plutoengine.graphics.texture.WrapMode;
import org.plutoengine.graphics.texture.texture2d.RectangleTexture;
import org.plutoengine.gui.font.CharacterInfo;
import org.plutoengine.gui.font.Font;
import org.plutoengine.logger.Logger;
import org.plutoengine.logger.SmartSeverity;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
public class FontManager
{
private static final Map<String, Font> fonts = new HashMap<>();
public static void loadFont(Path address)
{
String fontname = null;
int width = 0;
int height = 0;
var def = new HashMap<Character, CharacterInfo>();
int row = 0;
try
{
var lines = Files.readAllLines(address.resolve("definitions#txt"));
for (var line : lines)
{
if (line.startsWith("//"))
continue;
if (row == 0)
{
String[] fontinfo = line.split(",");
fontname = fontinfo[0];
String[] dim = fontinfo[1].split("x");
width = Integer.parseInt(dim[0]);
height = Integer.parseInt(dim[1]);
}
else
{
String[] offs = line.split(" ")[1].split(";");
def.put(line.charAt(0), new CharacterInfo(row - 1, Integer.parseInt(offs[0]), Integer.parseInt(offs[1])));
}
row++;
}
}
catch (Exception e)
{
Logger.log(SmartSeverity.ERROR, "Could not load font: " + address.toString());
e.printStackTrace();
}
Font font = new Font(fontname, width, height, def);
RectangleTexture texture = new RectangleTexture();
texture.load(address.resolve("tex#png"), MagFilter.NEAREST, MinFilter.LINEAR, WrapMode.CLAMP_TO_EDGE, WrapMode.CLAMP_TO_EDGE);
font.setTexture(texture);
fonts.put(fontname, font);
}
public static void unloadAll()
{
fonts.values()
.stream()
.map(Font::getTexture)
.forEach(Texture::delete);
fonts.clear();
}
public static Font getFontByName(String fontname)
{
var font = fonts.get(fontname);
if (font == null)
{
// Logger.log(SmartSeverity.WARNING, "Font with name " + fontname + " could not be found, using the default one instead (if there is one).");
return fonts.get("default");
}
return font;
}
}

View File

@ -1,5 +1,6 @@
package org.plutoengine.graphics.font; package org.plutoengine.graphics.gui;
import org.joml.Matrix3fc;
import org.plutoengine.graphics.gl.vao.attrib.ReservedAttributes; import org.plutoengine.graphics.gl.vao.attrib.ReservedAttributes;
import org.plutoengine.shader.ShaderBase; import org.plutoengine.shader.ShaderBase;
import org.plutoengine.shader.ShaderProgram; import org.plutoengine.shader.ShaderProgram;
@ -8,20 +9,14 @@ import org.plutoengine.shader.uniform.*;
import org.plutoengine.shader.uniform.auto.AutoViewportProjection; import org.plutoengine.shader.uniform.auto.AutoViewportProjection;
@ShaderProgram @ShaderProgram
public final class FontShader extends ShaderBase public final class FontShader extends ShaderBase implements IGUIShader
{ {
@AutoViewportProjection @AutoViewportProjection
@Uniform(name = "projection") @Uniform(name = "projection")
public UniformMat4 projectionMatrix; public UniformMat4 projectionMatrix;
@Uniform(name = "transformation") @Uniform(name = "transformation")
public UniformMat4 transformationMatrix; public UniformMat3 transformationMatrix;
@Uniform
public UniformVec2 uvBase;
@Uniform
public UniformVec2 uvDelta;
@Uniform @Uniform
public UniformRGBA recolor; public UniformRGBA recolor;
@ -34,4 +29,13 @@ public final class FontShader extends ShaderBase
@VertexArrayAttribute(ReservedAttributes.UV) @VertexArrayAttribute(ReservedAttributes.UV)
public int uvCoords; public int uvCoords;
@VertexArrayAttribute(2)
public int page;
@Override
public void setTransform(Matrix3fc transform)
{
this.transformationMatrix.load(transform);
}
} }

View File

@ -0,0 +1,8 @@
package org.plutoengine.graphics.gui;
import org.joml.Matrix3fc;
public interface IGUIShader
{
void setTransform(Matrix3fc transform);
}

View File

@ -0,0 +1,14 @@
package org.plutoengine.graphics.gui;
import org.plutoengine.component.PlutoLocalComponent;
public class PlutoGUI extends PlutoLocalComponent
{
@Override
public boolean isUnique()
{
return true;
}
}

View File

@ -0,0 +1,119 @@
package org.plutoengine.graphics.gui;
import org.plutoengine.graphics.gl.DrawMode;
import org.plutoengine.graphics.gl.vao.VertexArray;
import org.plutoengine.graphics.gl.vao.VertexArrayBuilder;
import org.plutoengine.graphics.gui.command.PlutoCommandDrawMesh;
import org.plutoengine.graphics.gui.command.PlutoCommandSwitchShader;
import org.plutoengine.graphics.gui.command.PlutoCommandSwitchTexture;
import org.plutoengine.libra.command.AbstractGUICommandParser;
import org.plutoengine.libra.command.IGUIRenderer;
import org.plutoengine.libra.command.LiCommandBuffer;
import org.plutoengine.libra.command.impl.LiCommandSetTransform;
import org.plutoengine.shader.ShaderBase;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashSet;
public class PlutoGUICommandParser extends AbstractGUICommandParser
{
@Override
protected IGUIRenderer parse(LiCommandBuffer mergedBuffer)
{
var drawCalls = new ArrayList<Runnable>();
var closeCalls = new ArrayDeque<Runnable>();
var alreadyEnabledAttribs = new HashSet<Integer>();
var currentShader = (ShaderBase & IGUIShader) null;
for (var cmd : mergedBuffer)
{
var type = cmd.getType();
switch (type)
{
case DRAW_MESH -> {
if (!(cmd instanceof PlutoCommandDrawMesh drawCmd))
throw new IllegalStateException();
var vab = new VertexArrayBuilder();
var data = drawCmd.getData();
var attrInfo = drawCmd.getAttributeInfo();
data.forEach((attr, val) -> vab.attrib(attr, attrInfo.get(attr).dimensions(), val.flip()));
var indices = drawCmd.getIndices();
if (indices != null)
vab.indices(indices.flip());
var vao = vab.build();
var attribs = vao.getUsedAttributes();
var attribsToEnable = new HashSet<>(attribs);
attribsToEnable.removeAll(alreadyEnabledAttribs);
alreadyEnabledAttribs.addAll(attribsToEnable);
drawCalls.add(() -> {
vao.bind();
attribsToEnable.forEach(VertexArray::enableAttribute);
vao.draw(DrawMode.TRIANGLES);
});
closeCalls.add(vao::close);
}
case SET_TRANSFORM -> {
if (!(cmd instanceof LiCommandSetTransform transformCmd))
throw new IllegalStateException();
var shaderCapture = currentShader;
assert shaderCapture != null;
drawCalls.add(() -> shaderCapture.setTransform(transformCmd.getTransform()));
}
case SWITCH_SHADER -> {
if (!(cmd instanceof PlutoCommandSwitchShader swSh))
throw new IllegalStateException();
var shaderCapture = currentShader = (ShaderBase & IGUIShader) swSh.getShader();
assert shaderCapture != null;
drawCalls.add(() -> shaderCapture.start());
}
case SWITCH_TEXTURE -> {
if (!(cmd instanceof PlutoCommandSwitchTexture swTx))
throw new IllegalStateException();
var textureCapture = swTx.getTexture();
assert textureCapture != null;
drawCalls.add(textureCapture::bind);
}
}
}
return new IGUIRenderer()
{
@Override
public void render()
{
drawCalls.forEach(Runnable::run);
}
@Override
public void close()
{
var it = closeCalls.descendingIterator();
while (it.hasNext())
{
var call = it.next();
call.run();
}
}
};
}
}

View File

@ -0,0 +1,12 @@
package org.plutoengine.graphics.gui;
import org.plutoengine.libra.command.LiCommandBuffer;
import org.plutoengine.libra.text.LiTextInfo;
public class PlutoTextInfo extends LiTextInfo
{
protected PlutoTextInfo(LiCommandBuffer commandBuffer)
{
super(commandBuffer);
}
}

View File

@ -0,0 +1,73 @@
package org.plutoengine.graphics.gui;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL33;
import org.plutoengine.graphics.texture.MagFilter;
import org.plutoengine.graphics.texture.MinFilter;
import org.plutoengine.graphics.texture.Texture;
import org.plutoengine.graphics.texture.WrapMode;
import org.plutoengine.tpl.ImageLoader;
import org.plutoengine.tpl.ImageY;
import java.awt.image.BufferedImage;
import java.nio.ByteBuffer;
import java.util.List;
public class SDFTextureArray extends Texture
{
public SDFTextureArray()
{
super(GL33.GL_TEXTURE_2D_ARRAY, 2);
}
@Override
public boolean supportsMipMapping()
{
return false;
}
@Override
public void writeData(long address)
{
GL33.glTexImage3D(GL33.GL_TEXTURE_2D_ARRAY, 0, GL33.GL_R8, this.width, this.height, this.depth, 0, GL33.GL_RED, GL11.GL_UNSIGNED_BYTE, address);
}
public void loadImg(List<BufferedImage> imageData, int width, int height, int depth, MagFilter magFilter, MinFilter minFilter, WrapMode... wrap)
{
var data = imageData.stream()
.map(ImageLoader::loadImageGrayscale)
.map(ImageY::getData)
.toList();
this.load(data, width, height, depth, magFilter, minFilter, wrap);
}
public void load(List<ByteBuffer> imageData, int width, int height, int depth, MagFilter magFilter, MinFilter minFilter, WrapMode... wrap)
{
this.width = width;
this.height = height;
this.depth = imageData.size();
this.bind();
this.setFilteringOptions(magFilter, minFilter);
this.setWrapOptions(wrap);
this.writeData(0);
for (int i = 0; i < imageData.size(); i++)
{
var img = imageData.get(i);
GL33.glTexSubImage3D(GL33.GL_TEXTURE_2D_ARRAY, 0,
0, 0, i,
this.width, this.height, 1,
GL33.GL_RED, GL11.GL_UNSIGNED_BYTE, img);
}
}
@Override
public void load(ByteBuffer imageData, int width, int height, MagFilter magFilter, MinFilter minFilter, WrapMode... wrap)
{
throw new UnsupportedOperationException();
}
}

View File

@ -0,0 +1,141 @@
package org.plutoengine.graphics.gui;
import org.lwjgl.system.MemoryUtil;
import org.plutoengine.graphics.PlutoGUIMod;
import org.plutoengine.graphics.gui.command.PlutoCommandDrawMesh;
import org.plutoengine.graphics.gui.command.PlutoCommandSwitchShader;
import org.plutoengine.graphics.gui.command.PlutoCommandSwitchTexture;
import org.plutoengine.libra.command.LiCommandBuffer;
import org.plutoengine.libra.text.LiTextInfo;
import org.plutoengine.libra.text.font.GlyphInfo;
import org.plutoengine.libra.text.shaping.IShapingStrategy;
import org.plutoengine.libra.text.shaping.TextShaper;
import java.util.Arrays;
import java.util.EnumSet;
public class STBTTBasicTextShaper implements IShapingStrategy<STBTTFont.STBTTGlyphMetrics, STBTTFont.STBTTGlyphAtlas, STBTTFont>
{
@Override
public LiTextInfo shape(EnumSet<TextShaper.EnumFeature> features, STBTTFont font, String text)
{
var commandBuf = new LiCommandBuffer();
var atlas = font.getGlyphAtlas();
var atlasTexture = atlas.getGlyphAtlasTexture();
var texSwitch = new PlutoCommandSwitchTexture(atlasTexture);
commandBuf.push(texSwitch);
var shader = PlutoGUIMod.fontShader;
var shaderSwitch = new PlutoCommandSwitchShader(shader);
commandBuf.push(shaderSwitch);
var cpCount = (int) text.codePoints().count();
var mesh = new PlutoCommandDrawMesh();
final var quadVerts = 4;
final var twoTriVerts = 6;
var vertDim = 2;
var vertexBuf = MemoryUtil.memAllocFloat(vertDim * quadVerts * cpCount);
var uvDim = 2;
var uvBuf = MemoryUtil.memAllocFloat(uvDim * quadVerts * cpCount);
var pageDim = 1;
var pageBuf = MemoryUtil.memAllocInt(pageDim * quadVerts * cpCount);
var indexBuf = MemoryUtil.memAllocInt(twoTriVerts * cpCount);
var cpIt = text.codePoints().iterator();
var indices = new int[] {
0, 1, 2,
0, 2, 3
};
float[] vertices = new float[vertDim * quadVerts];
float[] uvs = new float[uvDim * quadVerts];
int[] pages = new int[pageDim * quadVerts];
float scale = 1 / (float) STBTTFont.PIXEL_HEIGHT;
float x = 0;
float y = 0;
GlyphInfo<STBTTFont.STBTTGlyphAtlas, STBTTFont.STBTTGlyphMetrics> info;
STBTTFont.STBTTGlyphMetrics metrics = null;
int cp;
while (cpIt.hasNext())
{
cp = cpIt.next();
switch (cp)
{
case '\n' -> {
x = 0;
y += font.getLineAdvance() * scale;
continue;
}
}
if (metrics != null)
x += metrics.getKerning(cp);
metrics = font.getGlyphMetrics(cp);
info = atlas.getGlyph(cp);
if (metrics == null)
continue;
x += metrics.getLeftSideBearing() * scale;
if (info != null)
{
float gx = x;
float gy = y + font.getAscent() * scale - metrics.getCY0() * scale;
vertices[6] = vertices[0] = gx - STBTTFont.SDF_PADDING;
vertices[3] = vertices[1] = gy + STBTTFont.SDF_PADDING;
vertices[4] = vertices[2] = gx + metrics.getCX1() * scale - metrics.getCX0() * scale + STBTTFont.SDF_PADDING;
vertices[7] = vertices[5] = gy - metrics.getCY1() * scale + metrics.getCY0() * scale - STBTTFont.SDF_PADDING;
var uvRect = info.getRect();
uvs[6] = uvs[0] = uvRect.minX;
uvs[3] = uvs[1] = 1 - uvRect.maxY;
uvs[4] = uvs[2] = uvRect.maxX;
uvs[7] = uvs[5] = 1 - uvRect.minY;
Arrays.fill(pages, info.getPage());
vertexBuf.put(vertices);
uvBuf.put(uvs);
pageBuf.put(pages);
indexBuf.put(indices);
indices[0] += quadVerts;
indices[1] += quadVerts;
indices[2] += quadVerts;
indices[3] += quadVerts;
indices[4] += quadVerts;
indices[5] += quadVerts;
}
x += metrics.getAdvanceX() * scale;
}
mesh.addAttribute(shader.position, vertexBuf.flip(), vertDim);
mesh.addAttribute(shader.uvCoords, uvBuf.flip(), uvDim);
mesh.addAttribute(shader.page, pageBuf.flip(), pageDim);
mesh.addIndices(indexBuf.flip());
commandBuf.push(mesh);
return new PlutoTextInfo(commandBuf);
}
}

View File

@ -0,0 +1,358 @@
package org.plutoengine.graphics.gui;
import org.joml.primitives.Rectanglef;
import org.lwjgl.stb.*;
import org.lwjgl.system.MemoryStack;
import org.plutoengine.buffer.BufferHelper;
import org.plutoengine.graphics.texture.MagFilter;
import org.plutoengine.graphics.texture.MinFilter;
import org.plutoengine.graphics.texture.WrapMode;
import org.plutoengine.libra.text.font.GlyphInfo;
import org.plutoengine.libra.text.font.GlyphMetrics;
import org.plutoengine.libra.text.font.LiFont;
import org.plutoengine.logger.Logger;
import org.plutoengine.logger.SmartSeverity;
import java.awt.*;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.WritableRaster;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.IntStream;
public class STBTTFont extends LiFont<STBTTFont.STBTTGlyphAtlas, STBTTFont.STBTTGlyphMetrics> implements AutoCloseable
{
public static final int PIXEL_HEIGHT = 64;
public static final int SDF_PADDING = 4;
public static final int SHEET_SIZE = 1024;
private int descent;
private int ascent;
private int lineGap;
private int lineAdvance;
private Map<KerningPair, Integer> kerningTable;
private record KerningPair(int left, int right)
{
}
private STBTTFont(String name)
{
super(name);
}
public int getAscent()
{
return this.ascent;
}
public int getDescent()
{
return this.descent;
}
public int getLineGap()
{
return this.lineGap;
}
public int getLineAdvance()
{
return this.lineAdvance;
}
public int getKerningOffset(int left, int right)
{
return this.kerningTable.getOrDefault(new KerningPair(left, right), 0);
}
@Override
public void close()
{
var tex = this.getGlyphAtlas().getGlyphAtlasTexture();
tex.close();
}
public class STBTTGlyphAtlas extends LiFont<STBTTGlyphAtlas, STBTTGlyphMetrics>.GlyphAtlas
{
private SDFTextureArray glyphAtlasTexture;
public void setGlyphAtlasTexture(SDFTextureArray glyphAtlasTexture)
{
this.glyphAtlasTexture = glyphAtlasTexture;
}
public SDFTextureArray getGlyphAtlasTexture()
{
return this.glyphAtlasTexture;
}
}
public class STBTTGlyphMetrics extends GlyphMetrics
{
private final int codepoint;
private int advanceX;
private int leftSideBearing;
private int cx0;
private int cy0;
private int cx1;
private int cy1;
private int xOrigin;
private int yOrigin;
private STBTTGlyphMetrics(int codepoint)
{
this.codepoint = codepoint;
}
public int getAdvanceX()
{
return this.advanceX;
}
public int getLeftSideBearing()
{
return this.leftSideBearing;
}
public int getCodepoint()
{
return this.codepoint;
}
public int getKerning(int cp)
{
return STBTTFont.this.getKerningOffset(this.codepoint, cp);
}
public int getCX0()
{
return this.cx0;
}
public int getCY0()
{
return this.cy0;
}
public int getCX1()
{
return this.cx1;
}
public int getCY1()
{
return this.cy1;
}
public int getXOrigin()
{
return this.xOrigin;
}
public int getYOrigin()
{
return this.yOrigin;
}
}
public static STBTTFont load(Path path)
{
try (var stack = MemoryStack.stackPush())
{
var fontInfo = STBTTFontinfo.calloc(stack);
var data = BufferHelper.readToByteBuffer(path);
if (!STBTruetype.stbtt_InitFont(fontInfo, data))
{
Logger.logf(SmartSeverity.ERROR, "Failed to load font: %s%n", path);
return null;
}
float scale = STBTruetype.stbtt_ScaleForPixelHeight(fontInfo, PIXEL_HEIGHT);
var nameBuf = STBTruetype.stbtt_GetFontNameString(fontInfo,
STBTruetype.STBTT_PLATFORM_ID_MICROSOFT,
STBTruetype.STBTT_MS_EID_UNICODE_BMP,
STBTruetype.STBTT_MS_LANG_ENGLISH,
1);
if (nameBuf == null)
{
Logger.logf(SmartSeverity.ERROR, "Failed to load font: %s%n", path);
return null;
}
var name = StandardCharsets.UTF_16BE.decode(nameBuf).toString();
Logger.logf(SmartSeverity.ADDED, "Loading font: %s%n", name);
var font = new STBTTFont(name);
var ascentBuf = stack.callocInt(1);
var descentBuf = stack.callocInt(1);
var lineGapBuf = stack.callocInt(1);
STBTruetype.stbtt_GetFontVMetrics(fontInfo, ascentBuf, descentBuf, lineGapBuf);
var codepoints = IntStream.rangeClosed(0x0000, 0x04ff);
var onedgeValue = 220;
var pixelDistScale = onedgeValue / (float) SDF_PADDING;
var sdfWidthBuf = stack.mallocInt(1);
var sdfHeightBuf = stack.mallocInt(1);
var xOffsBuf = stack.mallocInt(1);
var yOffsBuf = stack.mallocInt(1);
var rectPacker = STBRPContext.malloc(stack);
var nodes = STBRPNode.calloc(SHEET_SIZE * 4);
STBRectPack.stbrp_init_target(rectPacker, SHEET_SIZE, SHEET_SIZE, nodes);
STBRectPack.stbrp_setup_allow_out_of_mem(rectPacker, true);
var rect = STBRPRect.malloc(1, stack);
var sheets = new ArrayList<BufferedImage>();
var atlas = new BufferedImage(SHEET_SIZE, SHEET_SIZE, BufferedImage.TYPE_BYTE_GRAY);
var graphics = atlas.getGraphics();
var colorSpace = ColorSpace.getInstance(ColorSpace.CS_GRAY);
var colorModel = new ComponentColorModel(colorSpace, new int[] { 8 }, false,false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
font.atlas = font.new STBTTGlyphAtlas();
var advanceWidth = stack.mallocInt(1);
var leftSideBearing = stack.mallocInt(1);
var cx0 = stack.mallocInt(1);
var cy0 = stack.mallocInt(1);
var cx1 = stack.mallocInt(1);
var cy1 = stack.mallocInt(1);
for (var cp : codepoints.toArray())
{
var buf = STBTruetype.stbtt_GetCodepointSDF(fontInfo,
scale,
cp,
SDF_PADDING,
(byte) onedgeValue,
pixelDistScale,
sdfWidthBuf,
sdfHeightBuf,
xOffsBuf,
yOffsBuf);
var width = sdfWidthBuf.get(0);
var height = sdfHeightBuf.get(0);
var glyphInfo = (GlyphInfo<STBTTGlyphAtlas, STBTTGlyphMetrics>) null;
if (buf != null)
{
rect.w(width);
rect.h(height);
if (STBRectPack.stbrp_pack_rects(rectPacker, rect) == 0)
{
sheets.add(atlas);
atlas = new BufferedImage(SHEET_SIZE, SHEET_SIZE, BufferedImage.TYPE_BYTE_GRAY);
graphics = atlas.getGraphics();
STBRectPack.stbrp_init_target(rectPacker, SHEET_SIZE, SHEET_SIZE, nodes);
STBRectPack.stbrp_setup_allow_out_of_mem(rectPacker, true);
STBRectPack.stbrp_pack_rects(rectPacker, rect);
}
var dataBuf = new DataBuffer(DataBuffer.TYPE_BYTE, width * height) {
@Override
public int getElem(int bank, int i)
{
return buf.get(i);
}
@Override
public void setElem(int bank, int i, int val)
{
buf.put(i, (byte) val);
}
};
var sampleModel = colorModel.createCompatibleSampleModel(width, height);
var raster = new WritableRaster(sampleModel, dataBuf, new Point()) {};
var image = new BufferedImage(colorModel, raster, false, null);
graphics.drawImage(image, rect.x(), rect.y(), null);
var glyphRect = new Rectanglef(
rect.x() / (float) SHEET_SIZE,
rect.y() / (float) SHEET_SIZE,
(rect.x() + rect.w()) / (float) SHEET_SIZE,
(rect.y() + rect.h()) / (float) SHEET_SIZE);
glyphInfo = new GlyphInfo<>(font.atlas, sheets.size(), glyphRect);
STBTruetype.stbtt_FreeSDF(buf);
}
STBTruetype.stbtt_GetCodepointHMetrics(fontInfo, cp, advanceWidth, leftSideBearing);
var glyphMetrics = font.new STBTTGlyphMetrics(cp);
glyphMetrics.advanceX = advanceWidth.get(0);
glyphMetrics.leftSideBearing = leftSideBearing.get(0);
glyphMetrics.xOrigin = xOffsBuf.get(0);
glyphMetrics.yOrigin = yOffsBuf.get(0);
STBTruetype.stbtt_GetCodepointBox(fontInfo, cp, cx0, cy0, cx1, cy1);
glyphMetrics.cx0 = cx0.get(0);
glyphMetrics.cy0 = cy0.get(0);
glyphMetrics.cx1 = cx1.get(0);
glyphMetrics.cy1 = cy1.get(0);
font.addGlyph(cp, glyphMetrics, glyphInfo);
}
if (!sheets.contains(atlas))
sheets.add(atlas);
nodes.free();
var kerningTableLength = STBTruetype.stbtt_GetKerningTableLength(fontInfo);
var kerningTable = STBTTKerningentry.malloc(kerningTableLength, stack);
STBTruetype.stbtt_GetKerningTable(fontInfo, kerningTable);
font.kerningTable = new HashMap<>();
kerningTable.forEach(e -> font.kerningTable.put(new KerningPair(e.glyph1(), e.glyph2()), e.advance()));
font.ascent = ascentBuf.get();
font.descent = descentBuf.get();
font.lineGap = lineGapBuf.get();
font.lineAdvance = font.ascent - font.descent + font.lineGap;
var tex = new SDFTextureArray();
tex.loadImg(sheets, SHEET_SIZE, SHEET_SIZE, sheets.size(), MagFilter.LINEAR, MinFilter.LINEAR, WrapMode.MIRROR_CLAMP_TO_EDGE, WrapMode.MIRROR_CLAMP_TO_EDGE);
font.atlas.setGlyphAtlasTexture(tex);
return font;
}
catch (Exception e)
{
Logger.logf(SmartSeverity.ERROR, "Failed to load font: %s%n", path);
Logger.log(e);
return null;
}
}
}

View File

@ -0,0 +1,161 @@
package org.plutoengine.graphics.gui.command;
import org.plutoengine.graphics.gl.vao.attrib.AttributeInfo;
import org.plutoengine.graphics.gl.vbo.EnumArrayBufferType;
import org.plutoengine.libra.command.impl.LiCommand;
import org.plutoengine.libra.command.impl.LiCommandDrawMesh;
import java.nio.Buffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.*;
public class PlutoCommandDrawMesh extends LiCommandDrawMesh
{
private final Map<Integer, AttributeInfo> attributeInfo;
private final Map<Integer, Buffer> data;
private IntBuffer indices;
public PlutoCommandDrawMesh()
{
this.attributeInfo = new TreeMap<>();
this.data = new TreeMap<>();
}
public IntBuffer getIndices()
{
if (this.indices == null)
return null;
return this.indices;
}
public Map<Integer, AttributeInfo> getAttributeInfo()
{
return Collections.unmodifiableMap(this.attributeInfo);
}
public Map<Integer, Buffer> getData()
{
return Collections.unmodifiableMap(this.data);
}
public void addIndices(int[] data)
{
if (data == null)
return;
this.addIndices(IntBuffer.wrap(data));
}
public void addIndices(IntBuffer data)
{
if (data == null)
return;
if (this.indices == null)
this.indices = IntBuffer.allocate(data.remaining());
if (this.indices.remaining() < data.remaining())
this.indices = IntBuffer.allocate(Math.max(this.indices.capacity() << 1, this.indices.capacity() + data.remaining())).put(this.indices.flip());
this.indices.put(data);
}
public void addAttribute(int attrib, float[] data, int dimensions)
{
if (data == null)
return;
this.addAttribute(attrib, FloatBuffer.wrap(data), dimensions);
}
public void addAttribute(int attrib, FloatBuffer data, int dimensions)
{
if (data == null)
return;
var meta = this.attributeInfo.computeIfAbsent(attrib, k -> new AttributeInfo(EnumArrayBufferType.FLOAT, attrib, dimensions));
if (meta.dimensions() != dimensions)
throw new IllegalArgumentException("Attribute dimensions mismatch!");
this.data.compute(attrib, (k, v) -> {
if (v == null)
return FloatBuffer.allocate(data.remaining()).put(data);
if (!(v instanceof FloatBuffer fab))
throw new IllegalArgumentException();
if (data.remaining() <= fab.remaining())
return fab.put(data);
var newBuf = FloatBuffer.allocate(Math.max(v.capacity() << 1, v.capacity() + data.remaining()));
return newBuf.put(fab.flip()).put(data);
});
}
public void addAttribute(int attrib, int[] data, int dimensions)
{
if (data == null)
return;
this.addAttribute(attrib, IntBuffer.wrap(data), dimensions);
}
public void addAttribute(int attrib, IntBuffer data, int dimensions)
{
if (data == null)
return;
var meta = this.attributeInfo.computeIfAbsent(attrib, k -> new AttributeInfo(EnumArrayBufferType.INT, attrib, dimensions));
if (meta.dimensions() != dimensions)
throw new IllegalArgumentException("Attribute dimensions mismatch!");
this.data.compute(attrib, (k, v) -> {
if (v == null)
return IntBuffer.allocate(data.remaining()).put(data);
if (!(v instanceof IntBuffer fab))
throw new IllegalArgumentException();
if (data.remaining() <= fab.remaining())
return fab.put(data);
var newBuf = IntBuffer.allocate(Math.max(v.capacity() << 1, v.capacity() + data.remaining()));
return newBuf.put(fab.flip()).put(data);
});
}
@Override
public boolean supportsMerge(LiCommand other)
{
if (!(other instanceof PlutoCommandDrawMesh pcdm))
return false;
return this.attributeInfo.equals(pcdm.attributeInfo);
}
@Override
public PlutoCommandDrawMesh merge(LiCommand other)
{
if (!(other instanceof PlutoCommandDrawMesh pcdm))
throw new UnsupportedOperationException();
pcdm.data.forEach((k, v) -> {
var attrInfo = this.attributeInfo.get(k);
switch (attrInfo.type())
{
case FLOAT -> this.addAttribute(k, (FloatBuffer) v, attrInfo.dimensions());
case INT -> this.addAttribute(k, (IntBuffer) v, attrInfo.dimensions());
case UNSIGNED_INT -> throw new UnsupportedOperationException();
}
});
this.addIndices(pcdm.indices);
return this;
}
}

View File

@ -0,0 +1,12 @@
package org.plutoengine.graphics.gui.command;
import org.plutoengine.graphics.gui.IGUIShader;
import org.plutoengine.libra.command.impl.LiCommandSwitchShader;
public class PlutoCommandSwitchShader extends LiCommandSwitchShader<IGUIShader>
{
public PlutoCommandSwitchShader(IGUIShader shader)
{
super(shader);
}
}

View File

@ -0,0 +1,12 @@
package org.plutoengine.graphics.gui.command;
import org.plutoengine.graphics.texture.Texture;
import org.plutoengine.libra.command.impl.LiCommandSwitchTexture;
public class PlutoCommandSwitchTexture extends LiCommandSwitchTexture<Texture>
{
public PlutoCommandSwitchTexture(Texture texture)
{
super(texture);
}
}

View File

@ -1,6 +0,0 @@
package org.plutoengine.gui.font;
public record CharacterInfo(int number, int leftOffset, int rightOffset)
{
}

View File

@ -1,59 +0,0 @@
package org.plutoengine.gui.font;
import org.plutoengine.graphics.texture.texture2d.RectangleTexture;
import java.util.HashMap;
public class Font
{
private String name;
private int width;
private int height;
private final HashMap<Character, CharacterInfo> definitions;
private RectangleTexture texture;
public Font(String name, int width, int height, HashMap<Character, CharacterInfo> def)
{
this.name = name;
this.width = width;
this.height = height;
this.definitions = def;
}
public String getName()
{
return this.name;
}
public void setName(String name)
{
this.name = name;
}
public int getTextureWidth()
{
return this.width;
}
public int getTextureHeight()
{
return this.height;
}
public RectangleTexture getTexture()
{
return this.texture;
}
public void setTexture(RectangleTexture texture)
{
this.texture = texture;
this.width = texture.getWidth();
this.height = texture.getHeight();
}
public HashMap<Character, CharacterInfo> getDefinitions()
{
return this.definitions;
}
}

View File

@ -1,120 +0,0 @@
package org.plutoengine.gui.font;
import org.plutoengine.graphics.font.FontManager;
public class FontHelper
{
public static int calcStringWidth(Object string, String fontname, float relativeSize)
{
Font font = FontManager.getFontByName(fontname);
if (font == null)
{
System.out.println("Font doesn't exist: " + fontname);
return -1;
}
float absoluteCharWidth = 16;
String text = string.toString();
int maxW = 0;
int totalSpacing = 0;
for (int i = 0; i < text.length(); i++)
{
if (text.length() > i + 1)
{
if (text.charAt(i) == '\\' && text.charAt(i + 1) == '&')
{
continue;
}
}
// &c[0xff770077]
if (text.length() > i + 13)
{
if (text.charAt(i) == '&' && text.charAt(i + 1) == 'c' && text.charAt(i + 2) == '[' && text.charAt(i + 13) == ']')
{
char c = '0';
char cBef = '0';
if (i > 0)
{
c = text.charAt(i - 1);
}
if (i > 1)
{
cBef = text.charAt(i - 2);
}
if (c != '\\' || cBef == '\\' && c == '\\')
{
i += 13;
continue;
}
}
}
if (text.length() > i + 2)
{
if (text.charAt(i) == '&' && text.charAt(i + 1) == 'i')
{
char c = '0';
char cBef = '0';
if (i > 0)
{
c = text.charAt(i - 1);
}
if (i > 1)
{
cBef = text.charAt(i - 2);
}
if (c != '\\' || cBef == '\\' && c == '\\')
{
i += 2;
continue;
}
}
}
if (text.charAt(i) == '\n')
{
totalSpacing = 0;
}
else if (text.charAt(i) == ' ')
{
totalSpacing += 12 * relativeSize;
}
else
{
CharacterInfo charInf = font.getDefinitions().get(text.charAt(i));
if (charInf == null)
{
charInf = font.getDefinitions().get('?');
}
totalSpacing -= charInf.leftOffset() * relativeSize - relativeSize;
totalSpacing += absoluteCharWidth * relativeSize;
totalSpacing -= charInf.rightOffset() * relativeSize - 1;
}
maxW = Math.max(maxW, totalSpacing);
}
return maxW;
}
public static int calcStringHeight(Object string, float relSize)
{
return (int) (30f * relSize * string.toString().split("\n").length);
}
}

View File

@ -1,287 +0,0 @@
package org.plutoengine.gui.font;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import org.lwjgl.opengl.GL11;
import org.plutoengine.graphics.font.FontManager;
import org.plutoengine.graphics.font.FontShader;
import org.plutoengine.graphics.gl.DrawMode;
import org.plutoengine.graphics.gl.vao.QuadPresets;
import org.plutoengine.graphics.gl.vao.VertexArray;
import org.plutoengine.math.TransformationMatrix;
public class FontRenderer
{
private static final int LINE_HEIGHT = 30;
private static final int SPACE_WIDTH = 12;
private static final int CHAR_WIDTH = 16;
private static final int CHAR_HEIGHT = 24;
private static VertexArray charVAO;
private static FontShader fontShader;
public static void load(FontShader defaultFontShaderIn)
{
charVAO = QuadPresets.basicNoNeg();
fontShader = defaultFontShaderIn;
}
public static void unload()
{
if (charVAO != null)
{
charVAO.delete();
}
}
public static void prepareInstance(Font font)
{
GL11.glEnable(GL11.GL_BLEND);
fontShader.start();
charVAO.bind();
charVAO.enableAllAttributes();
font.getTexture().bind();
}
public static void drawString(float xPos, float yPos, Object string, Object color, float relativeSize, String fontname, boolean isShadow)
{
Font font = FontManager.getFontByName(fontname);
if (font == null)
{
System.err.println("Font doesn't exist: " + fontname);
return;
}
float charWidth = CHAR_WIDTH * relativeSize;
float charHeight = CHAR_HEIGHT * relativeSize;
float lineHeight = LINE_HEIGHT * relativeSize;
float spaceWidth = SPACE_WIDTH * relativeSize;
prepareInstance(font);
color(color);
String text = String.valueOf(string);
float drawX = xPos;
float drawY = yPos;
for (int characterIndex = 0; characterIndex < text.length(); characterIndex++)
{
int column;
int row;
var currentChar = text.charAt(characterIndex);
if (text.length() > characterIndex + 1)
{
var nextChar = text.charAt(characterIndex + 1);
if (currentChar == '\\' && nextChar == '&')
{
continue;
}
// Inline coloring (tm) :) -> &c[0xff770077]
if (text.length() > characterIndex + 13)
{
if (currentChar == '&' && nextChar == 'c' && text.charAt(characterIndex + 2) == '[' && text.charAt(characterIndex + 13) == ']')
{
char c = '0';
char cBef = '0';
if (characterIndex > 0)
{
c = text.charAt(characterIndex - 1);
}
if (characterIndex > 1)
{
cBef = text.charAt(characterIndex - 2);
}
if (c != '\\' || cBef == '\\')
{
if (!isShadow)
{
color(text.substring(characterIndex + 3, characterIndex + 13));
}
characterIndex += 13;
continue;
}
}
}
if (text.length() > characterIndex + 2)
{
if (currentChar == '&' && nextChar == 'i' && (text.charAt(characterIndex + 2) == '1' || text.charAt(characterIndex + 2) == '0'))
{
char c = '0';
char cBef = '0';
if (characterIndex > 0)
{
c = text.charAt(characterIndex - 1);
}
if (characterIndex > 1)
{
cBef = text.charAt(characterIndex - 2);
}
if (c != '\\' || cBef == '\\')
{
italic(text.charAt(characterIndex + 2) == '1');
characterIndex += 2;
continue;
}
}
}
}
float shift = 0;
switch (currentChar)
{
case '\n' -> {
color(color);
drawX = xPos;
drawY += lineHeight;
continue;
}
case ' ' -> {
drawX += spaceWidth;
continue;
}
case 'g', 'y', 'p', 'j' -> shift = 6 * relativeSize;
default -> {
}
}
var fontDefs = font.getDefinitions();
var charInf = fontDefs.get(currentChar);
if (charInf == null)
{
charInf = fontDefs.get('?');
}
var atlasIndex = charInf.number();
row = atlasIndex / CHAR_WIDTH;
column = atlasIndex % CHAR_WIDTH;
// Position of the current character in the texture atlas in pixels
float u = column * CHAR_WIDTH;
float v = row * CHAR_HEIGHT;
// Offset from the left
drawX -= charInf.leftOffset() * relativeSize;
float posY = shift + drawY;
fontShader.uvBase.load(u, font.getTextureHeight() - v - CHAR_HEIGHT);
fontShader.uvDelta.load(CHAR_WIDTH, CHAR_HEIGHT);
Matrix4f transformation = TransformationMatrix.create(new Vector3f(drawX, posY, 0), new Vector3f(0, 0, 0), new Vector3f(charWidth, charHeight, 0));
fontShader.transformationMatrix.load(transformation);
charVAO.draw(DrawMode.TRIANGLES);
drawX += charWidth;
drawX -= charInf.rightOffset() * relativeSize;
drawX += relativeSize;
}
italic(false);
}
public static void color(Object color)
{
color(color, false);
}
public static void color(Object color, boolean darker)
{
float dark = 0;
if (darker)
{
dark = 0.35f;
}
if (color instanceof float[] c)
{
if (c.length == 4)
{
recolor(c[0] - dark, c[1] - dark, c[2] - dark, c[3]);
}
else if (c.length == 3)
{
recolor(c[0] - dark, c[1] - dark, c[2] - dark, 1);
}
}
if (color instanceof String col)
{
if (col.length() == 7)
{
recolor(Integer.valueOf(col.substring(1, 3), 16) / 256f - dark, Integer.valueOf(col.substring(3, 5), 16) / 256f - dark, Integer.valueOf(col.substring(5, 7), 16) / 256f - dark, 1);
}
if (col.length() == 10)
{
recolor(Integer.valueOf(col.substring(4, 6), 16) / 256f - dark, Integer.valueOf(col.substring(6, 8), 16) / 256f - dark, Integer.valueOf(col.substring(8, 10), 16) / 256f - dark, Integer.valueOf(col.substring(2, 4), 16) / 256f);
}
}
}
private static void recolor(float r, float g, float b, float a)
{
fontShader.recolor.load(r, g, b, a);
}
private static void italic(boolean useItalic)
{
fontShader.italic.load(useItalic);
}
public static void drawString(float x, float y, Object text, float r, float g, float b, float a, float size)
{
drawString(x, y, text, new float[] { r, g, b, a }, size, "default", false);
}
public static void drawString(float x, float y, Object text)
{
drawString(x, y, text, new float[] { 0, 0, 0, 1 }, 1, "default", false);
}
public static void drawString(float x, float y, Object text, float r, float g, float b, float a, float size, boolean isShadow)
{
drawString(x, y, text, new float[] { r, g, b, a }, size, "default", isShadow);
}
public static void drawString(float x, float y, Object text, float r, float g, float b, float a, float size, String fontname, boolean isShadow)
{
drawString(x, y, text, new float[] { r, g, b, a }, size, fontname, isShadow);
}
public static void drawString(float x, float y, Object text, float r, float g, float b, float a, float size, String fontname)
{
drawString(x, y, text, new float[] { r, g, b, a }, size, fontname, false);
}
public static void drawString(float xPos, float yPos, Object string, Object color, float relativeSize, String fontname)
{
drawString(xPos, yPos, string, color, relativeSize, fontname, false);
}
}

View File

@ -18,6 +18,8 @@ dependencies {
api("com.google.guava", "guava", "28.0-jre") api("com.google.guava", "guava", "28.0-jre")
api("org.joml", "joml", Versions.jomlVersion) api("org.joml", "joml", Versions.jomlVersion)
api("org.joml", "joml-primitives", Versions.jomlPrimitivesVersion)
api("commons-io", "commons-io", "2.6") api("commons-io", "commons-io", "2.6")
api("org.apache.commons", "commons-lang3", "3.12.0") api("org.apache.commons", "commons-lang3", "3.12.0")

View File

@ -2,7 +2,7 @@ package org.plutoengine.event.lambda;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Predicate;
/** /**
* A simple functional interface based event factory for objects basically * A simple functional interface based event factory for objects basically
@ -25,7 +25,7 @@ public class LambdaEventFactory
*/ */
public static class LambdaEvent<T> public static class LambdaEvent<T>
{ {
private final List<Consumer<T>> consumers; private final List<Predicate<T>> consumers;
private LambdaEvent() private LambdaEvent()
{ {
@ -42,7 +42,7 @@ public class LambdaEventFactory
* *
* @author 493msi * @author 493msi
*/ */
public void addListener(Consumer<T> callback) public void addListener(Predicate<T> callback)
{ {
this.consumers.add(callback); this.consumers.add(callback);
} }
@ -57,7 +57,7 @@ public class LambdaEventFactory
* *
* @author 493msi * @author 493msi
*/ */
public void removeListener(Consumer<T> callback) public void removeListener(Predicate<T> callback)
{ {
this.consumers.remove(callback); this.consumers.remove(callback);
} }
@ -74,7 +74,7 @@ public class LambdaEventFactory
*/ */
public void fire(T value) public void fire(T value)
{ {
this.consumers.forEach(c -> c.accept(value)); this.consumers.removeIf(c -> !c.test(value));
} }
} }

View File

@ -1,7 +1,5 @@
package org.plutoengine.graphics.gl.vao; package org.plutoengine.graphics.gl.vao;
import org.plutoengine.graphics.gl.vao.attrib.data.VecArray;
/** /**
* @author 493msi * @author 493msi
* *
@ -10,48 +8,85 @@ public class QuadPresets
{ {
public static VertexArray basicQuad() public static VertexArray basicQuad()
{ {
float[] uvs = { 0, 0, 1, 0, 1, 1, 0, 1 }; float[] uvs = {
0, 0,
1, 0,
1, 1,
0, 1 };
float[] positions = { -1, 1, 1, 1, 1, -1, -1, -1 }; float[] positions = {
-1, 1,
1, 1,
1, -1,
-1, -1 };
int[] indices = { 0, 1, 2, 0, 2, 3 }; int[] indices = {
0, 1, 2,
0, 2, 3
};
VertexArrayBuilder vab = new VertexArrayBuilder(); VertexArrayBuilder vab = new VertexArrayBuilder();
vab.vertices(new VecArray<>(positions, 2)); vab.vertices(positions, 2);
vab.uvs(new VecArray<>(uvs, 2)); vab.uvs(uvs, 2);
vab.indices(new VecArray<>(indices, 1)); vab.indices(indices);
return vab.export(); return vab.build();
} }
public static VertexArray halvedSize() public static VertexArray halvedSize()
{ {
float[] uvs = { 0, 0, 1, 0, 1, 1, 0, 1 }; float[] uvs = {
0, 0,
1, 0,
1, 1,
0, 1
};
float[] positions = { -0.5f, 0.5f, 0.5f, 0.5f, 0.5f, -0.5f, -0.5f, -0.5f }; float[] positions = {
-0.5f, 0.5f,
0.5f, 0.5f,
0.5f, -0.5f,
-0.5f, -0.5f
};
int[] indices = { 0, 1, 2, 0, 2, 3 }; int[] indices = {
0, 1, 2,
0, 2, 3
};
VertexArrayBuilder vab = new VertexArrayBuilder(); VertexArrayBuilder vab = new VertexArrayBuilder();
vab.vertices(new VecArray<>(positions, 2)); vab.vertices(positions, 2);
vab.uvs(new VecArray<>(uvs, 2)); vab.uvs(uvs, 2);
vab.indices(new VecArray<>(indices, 1)); vab.indices(indices);
return vab.export(); return vab.build();
} }
public static VertexArray basicNoNeg() public static VertexArray basicNoNeg()
{ {
float[] uvs = { 0, 0, 1, 0, 1, 1, 0, 1 }; float[] uvs = {
0, 0,
1, 0,
1, 1,
0, 1
};
float[] positions = { 0, 1, 1, 1, 1, 0, 0, 0 }; float[] positions = {
0, 1,
1, 1,
1, 0,
0, 0
};
int[] indices = { 0, 1, 2, 0, 2, 3 }; int[] indices = {
0, 1, 2,
0, 2, 3
};
VertexArrayBuilder vab = new VertexArrayBuilder(); VertexArrayBuilder vab = new VertexArrayBuilder();
vab.vertices(new VecArray<>(positions, 2)); vab.vertices(positions, 2);
vab.uvs(new VecArray<>(uvs, 2)); vab.uvs(uvs, 2);
vab.indices(new VecArray<>(indices, 1)); vab.indices(indices);
return vab.export(); return vab.build();
} }
} }

View File

@ -3,18 +3,16 @@ package org.plutoengine.graphics.gl.vao;
import org.lwjgl.opengl.GL33; import org.lwjgl.opengl.GL33;
import org.lwjgl.system.MemoryUtil; import org.lwjgl.system.MemoryUtil;
import org.plutoengine.graphics.gl.DrawMode; import org.plutoengine.graphics.gl.DrawMode;
import org.plutoengine.graphics.gl.vao.attrib.AttributeInfo;
import org.plutoengine.graphics.gl.vbo.ArrayBuffer; import org.plutoengine.graphics.gl.vbo.ArrayBuffer;
import org.plutoengine.graphics.gl.vbo.IndexArrayBuffer; import org.plutoengine.graphics.gl.vbo.IndexArrayBuffer;
import java.util.ArrayList; import java.util.*;
import java.util.Collections;
import java.util.List;
import java.util.Vector;
import org.plutoengine.logger.Logger; import org.plutoengine.logger.Logger;
import org.plutoengine.logger.SmartSeverity; import org.plutoengine.logger.SmartSeverity;
public class VertexArray public class VertexArray implements AutoCloseable
{ {
protected final List<Integer> usedAttributes; protected final List<Integer> usedAttributes;
protected final Vector<ArrayBuffer<?>> vertexAttributes; protected final Vector<ArrayBuffer<?>> vertexAttributes;
@ -34,22 +32,29 @@ public class VertexArray
this.glID = GL33.glGenVertexArrays(); this.glID = GL33.glGenVertexArrays();
Logger.logf(SmartSeverity.ADDED, "Vertex array ID %d created...\n", this.glID); Logger.logf(SmartSeverity.ADDED, "Vertex array ID %d created...%n", this.glID);
} }
public void createArrayAttribute(ArrayBuffer<?> buffer, int attribID) public void createArrayAttribute(AttributeInfo info, ArrayBuffer<?> buffer)
{ {
var attribID = info.position();
var dimensions = info.dimensions();
var type = buffer.getDataType();
this.bind(); this.bind();
buffer.bind(); buffer.bind();
GL33.glVertexAttribPointer(attribID, buffer.getVertexDimensions(), buffer.getType().getGLID(), false, 0, 0); GL33.glVertexAttribPointer(attribID, dimensions, type.getGLID(), false, 0, 0);
this.vertexAttributes.set(attribID, buffer); this.vertexAttributes.set(attribID, buffer);
this.usedAttributes.add(attribID); this.usedAttributes.add(attribID);
if (!this.hasIndices()) if (!this.hasIndices())
{ this.vertexCount = buffer.getSize() / dimensions;
this.vertexCount = buffer.getVertexCount();
} }
public Set<Integer> getUsedAttributes()
{
return Set.copyOf(this.usedAttributes);
} }
public List<ArrayBuffer<?>> getVertexAttributes() public List<ArrayBuffer<?>> getVertexAttributes()
@ -64,7 +69,7 @@ public class VertexArray
public void enableAllAttributes() public void enableAllAttributes()
{ {
this.usedAttributes.forEach(GL33::glEnableVertexAttribArray); this.usedAttributes.forEach(VertexArray::enableAttribute);
} }
public void bindIndices(IndexArrayBuffer buffer) public void bindIndices(IndexArrayBuffer buffer)
@ -72,7 +77,7 @@ public class VertexArray
this.bind(); this.bind();
buffer.bind(); buffer.bind();
this.indices = buffer; this.indices = buffer;
this.vertexCount = buffer.getVertexCount(); this.vertexCount = buffer.getSize();
} }
public void bind() public void bind()
@ -89,7 +94,7 @@ public class VertexArray
{ {
if (this.hasIndices()) if (this.hasIndices())
{ {
GL33.glDrawElements(mode.getGLID(), this.vertexCount, this.indices.getType().getGLID(), MemoryUtil.NULL); GL33.glDrawElements(mode.getGLID(), this.vertexCount, this.indices.getDataType().getGLID(), MemoryUtil.NULL);
} }
else else
{ {
@ -101,7 +106,7 @@ public class VertexArray
{ {
if (this.hasIndices()) if (this.hasIndices())
{ {
GL33.glDrawElementsInstanced(mode.getGLID(), this.vertexCount, this.indices.getType().getGLID(), MemoryUtil.NULL, count); GL33.glDrawElementsInstanced(mode.getGLID(), this.vertexCount, this.indices.getDataType().getGLID(), MemoryUtil.NULL, count);
} }
else else
{ {
@ -119,15 +124,17 @@ public class VertexArray
return this.indices != null; return this.indices != null;
} }
public void delete() public void close()
{ {
this.usedAttributes.stream().map(this.vertexAttributes::get).forEach(ArrayBuffer::delete); this.usedAttributes.stream()
.map(this.vertexAttributes::get)
.forEach(ArrayBuffer::close);
this.vertexAttributes.clear(); this.vertexAttributes.clear();
this.usedAttributes.clear(); this.usedAttributes.clear();
if (this.indices != null) if (this.indices != null)
{ {
this.indices.delete(); this.indices.close();
this.indices = null; this.indices = null;
} }
@ -142,4 +149,14 @@ public class VertexArray
{ {
return this.glID; return this.glID;
} }
public static void enableAttribute(int attribute)
{
GL33.glEnableVertexAttribArray(attribute);
}
public static void disableAttribute(int attribute)
{
GL33.glDisableVertexAttribArray(attribute);
}
} }

View File

@ -1,9 +1,12 @@
package org.plutoengine.graphics.gl.vao; package org.plutoengine.graphics.gl.vao;
import org.plutoengine.graphics.gl.vao.attrib.AttributeInfo;
import org.plutoengine.graphics.gl.vao.attrib.ReservedAttributes; import org.plutoengine.graphics.gl.vao.attrib.ReservedAttributes;
import org.plutoengine.graphics.gl.vao.attrib.data.VecArray; import org.plutoengine.graphics.gl.vbo.*;
import org.plutoengine.graphics.gl.vbo.FloatArrayBuffer;
import org.plutoengine.graphics.gl.vbo.IndexArrayBuffer; import java.nio.Buffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
public class VertexArrayBuilder public class VertexArrayBuilder
{ {
@ -14,28 +17,68 @@ public class VertexArrayBuilder
this.va = new VertexArray(); this.va = new VertexArray();
} }
public VertexArrayBuilder vertices(VecArray<float[]> vertices) public VertexArrayBuilder vertices(FloatBuffer vertices, int dimensions)
{ {
this.va.createArrayAttribute(new FloatArrayBuffer(vertices), ReservedAttributes.POSITION); return this.attrib(ReservedAttributes.POSITION, dimensions, vertices);
}
public VertexArrayBuilder uvs(FloatBuffer uvs, int dimensions)
{
return this.attrib(ReservedAttributes.UV, dimensions, uvs);
}
public VertexArrayBuilder colors(FloatBuffer colors, int dimensions)
{
return this.attrib(ReservedAttributes.COLOR, dimensions, colors);
}
public VertexArrayBuilder vertices(float[] vertices, int dimensions)
{
return this.vertices(FloatBuffer.wrap(vertices), dimensions);
}
public VertexArrayBuilder uvs(float[] uvs, int dimensions)
{
return this.uvs(FloatBuffer.wrap(uvs), dimensions);
}
public VertexArrayBuilder colors(float[] colors, int dimensions)
{
return this.colors(FloatBuffer.wrap(colors), dimensions);
}
public VertexArrayBuilder attrib(int attribute, int dimensions, Buffer data)
{
if (data instanceof FloatBuffer fab)
{
var attrInfo = new AttributeInfo(EnumArrayBufferType.FLOAT, attribute, dimensions);
var attr = FloatArrayBuffer.from(fab);
this.va.createArrayAttribute(attrInfo, attr);
}
else if (data instanceof IntBuffer iab)
{
var attrInfo = new AttributeInfo(EnumArrayBufferType.INT, attribute, dimensions);
var attr = IntArrayBuffer.from(iab);
this.va.createArrayAttribute(attrInfo, attr);
}
return this; return this;
} }
public VertexArrayBuilder uvs(VecArray<float[]> uvs) public VertexArrayBuilder indices(int[] indices)
{ {
this.va.createArrayAttribute(new FloatArrayBuffer(uvs), ReservedAttributes.UV); return this.indices(IntBuffer.wrap(indices));
}
public VertexArrayBuilder indices(IntBuffer indices)
{
var data = IndexArrayBuffer.from(indices);
this.va.bindIndices(data);
return this; return this;
} }
public VertexArrayBuilder indices(VecArray<int[]> indices) public VertexArray build()
{
this.va.bindIndices(new IndexArrayBuffer(indices));
return this;
}
public VertexArray export()
{ {
return this.va; return this.va;
} }

View File

@ -0,0 +1,12 @@
package org.plutoengine.graphics.gl.vao.attrib;
import org.intellij.lang.annotations.MagicConstant;
import org.plutoengine.graphics.gl.vbo.EnumArrayBufferType;
public record AttributeInfo(
EnumArrayBufferType type,
@MagicConstant(valuesFromClass = ReservedAttributes.class) int position,
int dimensions
)
{
}

View File

@ -1,46 +0,0 @@
package org.plutoengine.graphics.gl.vao.attrib.data;
import java.lang.reflect.Array;
public class VecArray<T>
{
private final T data;
private final int vecDimensions;
private final int vertexCount;
public VecArray(T data, int vecDimensions)
{
var dataType = data.getClass();
if (!dataType.isArray())
{
throw new IllegalStateException("Input data must be of an array type!");
}
this.data = data;
this.vecDimensions = vecDimensions;
var dataLength = Array.getLength(data);
if (dataLength % vecDimensions != 0)
{
throw new IllegalArgumentException("Data size must be divisible by the amount of vector dimensions!");
}
this.vertexCount = dataLength / vecDimensions;
}
public T getData()
{
return this.data;
}
public int getVecDimensions()
{
return this.vecDimensions;
}
public int getVertexCount()
{
return this.vertexCount;
}
}

View File

@ -1,43 +1,45 @@
package org.plutoengine.graphics.gl.vbo; package org.plutoengine.graphics.gl.vbo;
import org.plutoengine.graphics.gl.vao.attrib.data.VecArray; import org.lwjgl.opengl.GL33;
import static org.lwjgl.opengl.GL15.*; import java.nio.Buffer;
public abstract class ArrayBuffer<T extends VecArray<?>> public sealed abstract class ArrayBuffer<T extends Buffer> implements AutoCloseable permits FloatArrayBuffer, IndexArrayBuffer, IntArrayBuffer
{ {
protected int glID; protected int glID;
private final int vertexDimensions; protected final int type;
private final int vertexCount; private final int size;
public ArrayBuffer(T data) protected ArrayBuffer(int type, int size)
{ {
this.glID = glGenBuffers(); this.glID = GL33.glGenBuffers();
this.bind(); this.type = type;
this.bindData(data); this.size = size;
this.vertexDimensions = data.getVecDimensions();
this.vertexCount = data.getVertexCount();
} }
public abstract EnumArrayBufferType getType(); public abstract EnumArrayBufferType getDataType();
protected abstract void bindData(T data); public abstract void writePartialData(T data, long offset);
public int getSize()
{
return this.size;
}
public void bind() public void bind()
{ {
glBindBuffer(GL_ARRAY_BUFFER, this.glID); GL33.glBindBuffer(this.type, this.glID);
} }
public void unbind() public void unbind()
{ {
glBindBuffer(GL_ARRAY_BUFFER, 0); GL33.glBindBuffer(this.type, 0);
} }
public void delete() public void close()
{ {
glDeleteBuffers(this.glID); GL33.glDeleteBuffers(this.glID);
this.glID = 0; this.glID = 0;
} }
@ -46,14 +48,4 @@ public abstract class ArrayBuffer<T extends VecArray<?>>
{ {
return this.glID; return this.glID;
} }
public int getVertexDimensions()
{
return this.vertexDimensions;
}
public int getVertexCount()
{
return this.vertexCount;
}
} }

View File

@ -1,24 +1,60 @@
package org.plutoengine.graphics.gl.vbo; package org.plutoengine.graphics.gl.vbo;
import org.lwjgl.opengl.GL33; import org.lwjgl.opengl.GL33;
import org.plutoengine.graphics.gl.vao.attrib.data.VecArray; import org.lwjgl.system.MemoryUtil;
public class FloatArrayBuffer extends ArrayBuffer<VecArray<float[]>> import java.nio.FloatBuffer;
public final class FloatArrayBuffer extends ArrayBuffer<FloatBuffer>
{ {
public FloatArrayBuffer(VecArray<float[]> data) private FloatArrayBuffer(int size)
{ {
super(data); super(GL33.GL_ARRAY_BUFFER, size);
} }
@Override @Override
protected void bindData(VecArray<float[]> vertexData) public void writePartialData(FloatBuffer data, long offset)
{ {
GL33.glBufferData(GL33.GL_ARRAY_BUFFER, vertexData.getData(), GL33.GL_STATIC_DRAW); GL33.glBufferSubData(this.type, offset, data);
} }
@Override @Override
public EnumArrayBufferType getType() public EnumArrayBufferType getDataType()
{ {
return EnumArrayBufferType.FLOAT; return EnumArrayBufferType.FLOAT;
} }
public static FloatArrayBuffer from(FloatBuffer data)
{
if (!data.isDirect())
{
var fb = MemoryUtil.memAllocFloat(data.remaining());
fb.put(data);
fb.flip();
var fab = from(fb);
MemoryUtil.memFree(fb);
return fab;
}
var fab = new FloatArrayBuffer(data.remaining());
fab.bind();
GL33.glBufferData(fab.type, data, GL33.GL_STATIC_DRAW);
return fab;
}
public static FloatArrayBuffer from(float[] data)
{
var fab = new FloatArrayBuffer(data.length);
fab.bind();
GL33.glBufferData(fab.type, data, GL33.GL_STATIC_DRAW);
return fab;
}
public static FloatArrayBuffer empty(int size)
{
var fab = new FloatArrayBuffer(size);
fab.bind();
GL33.glBufferData(fab.type, size, GL33.GL_STATIC_DRAW);
return fab;
}
} }

View File

@ -1,50 +1,60 @@
package org.plutoengine.graphics.gl.vbo; package org.plutoengine.graphics.gl.vbo;
import static org.lwjgl.opengl.GL15.GL_ELEMENT_ARRAY_BUFFER; import org.lwjgl.opengl.GL33;
import static org.lwjgl.opengl.GL15.GL_STATIC_DRAW; import org.lwjgl.system.MemoryUtil;
import static org.lwjgl.opengl.GL15.glBindBuffer;
import static org.lwjgl.opengl.GL15.glBufferData;
import org.plutoengine.graphics.gl.vao.attrib.data.VecArray; import java.nio.IntBuffer;
public class IndexArrayBuffer extends ArrayBuffer<VecArray<int[]>> public final class IndexArrayBuffer extends ArrayBuffer<IntBuffer>
{ {
public IndexArrayBuffer(int[] data) private IndexArrayBuffer(int size)
{ {
super(new VecArray<>(data, 1)); super(GL33.GL_ELEMENT_ARRAY_BUFFER, size);
}
public IndexArrayBuffer(VecArray<int[]> data)
{
super(data);
if (data.getVecDimensions() != 1)
{
throw new IllegalArgumentException("Index buffers must have exactly one vertex dimension!");
}
} }
@Override @Override
public void bind() public void writePartialData(IntBuffer data, long offset)
{ {
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, this.glID); GL33.glBufferSubData(this.type, offset, data);
} }
@Override @Override
protected void bindData(VecArray<int[]> data) public EnumArrayBufferType getDataType()
{
glBufferData(GL_ELEMENT_ARRAY_BUFFER, data.getData(), GL_STATIC_DRAW);
}
@Override
public void unbind()
{
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}
@Override
public EnumArrayBufferType getType()
{ {
return EnumArrayBufferType.UNSIGNED_INT; return EnumArrayBufferType.UNSIGNED_INT;
} }
public static IndexArrayBuffer from(IntBuffer data)
{
if (!data.isDirect())
{
var ib = MemoryUtil.memAllocInt(data.remaining());
ib.put(data);
ib.flip();
var iab = from(ib);
MemoryUtil.memFree(ib);
return iab;
}
var iab = new IndexArrayBuffer(data.remaining());
iab.bind();
GL33.glBufferData(iab.type, data, GL33.GL_STATIC_DRAW);
return iab;
}
public static IndexArrayBuffer from(int[] data)
{
var iab = new IndexArrayBuffer(data.length);
iab.bind();
GL33.glBufferData(iab.type, data, GL33.GL_STATIC_DRAW);
return iab;
}
public static IndexArrayBuffer empty(int size)
{
var iab = new IndexArrayBuffer(size);
iab.bind();
GL33.glBufferData(iab.type, size, GL33.GL_DYNAMIC_DRAW);
return iab;
}
} }

View File

@ -0,0 +1,60 @@
package org.plutoengine.graphics.gl.vbo;
import org.lwjgl.opengl.GL33;
import org.lwjgl.system.MemoryUtil;
import java.nio.IntBuffer;
public non-sealed class IntArrayBuffer extends ArrayBuffer<IntBuffer>
{
private IntArrayBuffer(int size)
{
super(GL33.GL_ARRAY_BUFFER, size);
}
@Override
public void writePartialData(IntBuffer data, long offset)
{
GL33.glBufferSubData(this.type, offset, data);
}
@Override
public EnumArrayBufferType getDataType()
{
return EnumArrayBufferType.INT;
}
public static IntArrayBuffer from(IntBuffer data)
{
if (!data.isDirect())
{
var ib = MemoryUtil.memAllocInt(data.remaining());
ib.put(data);
ib.flip();
var iab = from(ib);
MemoryUtil.memFree(ib);
return iab;
}
var iab = new IntArrayBuffer(data.remaining());
iab.bind();
GL33.glBufferData(iab.type, data, GL33.GL_STATIC_DRAW);
return iab;
}
public static IntArrayBuffer from(int[] data)
{
var iab = new IntArrayBuffer(data.length);
iab.bind();
GL33.glBufferData(iab.type, data, GL33.GL_STATIC_DRAW);
return iab;
}
public static IntArrayBuffer empty(int size)
{
var iab = new IntArrayBuffer(size);
iab.bind();
GL33.glBufferData(iab.type, size, GL33.GL_DYNAMIC_DRAW);
return iab;
}
}

View File

@ -208,6 +208,8 @@ public final class ModLoader extends PlutoLocalComponent
this.loadedModStack.addLast(mod); this.loadedModStack.addLast(mod);
} }
this.loadingPhase = EnumModLoadingPhase.DONE;
} }
catch (Exception e) catch (Exception e)
{ {

View File

@ -35,7 +35,10 @@ public enum EnumBackingFileSystem
return fs.getPath(path); return fs.getPath(path);
}), }),
FS_ZIP("zip", FS_ZIP("zip",
uri -> FileSystems.newFileSystem(uri, Map.of()), uri -> {
var provider = FileSystems.getDefault().provider();
return FileSystems.newFileSystem(provider.getPath(uri), Map.of(), null);
},
(uri, fileSystem) -> fileSystem.getPath("/"), (uri, fileSystem) -> fileSystem.getPath("/"),
(fs, address, ext) -> { (fs, address, ext) -> {
var sep = fs.getSeparator(); var sep = fs.getSeparator();

View File

@ -4,7 +4,7 @@ import java.awt.image.BufferedImage;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
/** /**
* A wrapper around a native color buffer for easier handling * A wrapper around a native ABGR buffer for easier handling
* by various APIs, such as OpenGL and GLFW. * by various APIs, such as OpenGL and GLFW.
* *
* TPNImage is <em>always</em> assumed to be ABGR due to image format * TPNImage is <em>always</em> assumed to be ABGR due to image format

View File

@ -9,16 +9,18 @@ import java.nio.ByteOrder;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import org.lwjgl.BufferUtils;
import org.plutoengine.logger.Logger; import org.plutoengine.logger.Logger;
import org.plutoengine.logger.SmartSeverity; import org.plutoengine.logger.SmartSeverity;
/** /**
* Quick ABGR (8-bit per channel, 32 bits per pixel) image loader for OpenGL textures. * Quick ABGR (8-bit per channel, 32 bits per pixel) and grayscale image loader for OpenGL textures.
* Color component swizzling may be needed. * Color component swizzling may be needed.
* *
* @author 493msi * @author 493msi
* *
* @see ImageABGR * @see ImageABGR
* @see ImageY
* *
* @since pre-alpha * @since pre-alpha
*/ */
@ -34,8 +36,6 @@ public class ImageLoader
static static
{ {
placeholder = new BufferedImage(PLACEHOLDER_SIZE, PLACEHOLDER_SIZE, BufferedImage.TYPE_INT_ARGB); placeholder = new BufferedImage(PLACEHOLDER_SIZE, PLACEHOLDER_SIZE, BufferedImage.TYPE_INT_ARGB);
var data = placeholder.getData();
var dataBuffer = (DataBufferInt) data.getDataBuffer();
for (int i = 0; i < PLACEHOLDER_SIZE * PLACEHOLDER_SIZE; i++) for (int i = 0; i < PLACEHOLDER_SIZE * PLACEHOLDER_SIZE; i++)
{ {
@ -43,7 +43,7 @@ public class ImageLoader
int y = i / PLACEHOLDER_SIZE; int y = i / PLACEHOLDER_SIZE;
boolean checker = x / PLACEHOLDER_CHECKEDBOARD_SQUARE_SIZE % 2 == y / PLACEHOLDER_CHECKEDBOARD_SQUARE_SIZE % 2; boolean checker = x / PLACEHOLDER_CHECKEDBOARD_SQUARE_SIZE % 2 == y / PLACEHOLDER_CHECKEDBOARD_SQUARE_SIZE % 2;
dataBuffer.setElem(i, checker ? 0xFFFF0000 : 0xFF000000); placeholder.setRGB(x, y, checker ? 0xFFFF0000 : 0xFF000000);
} }
} }
@ -105,7 +105,7 @@ public class ImageLoader
/** /**
* Writes a {@link BufferedImage} into a {@link ImageABGR} buffer. * Writes a {@link BufferedImage} into a {@link ImageABGR} buffer.
* *
* If the input {@link Path} is null, a placeholder will be generated. * If the input {@link BufferedImage} is null, a placeholder will be generated.
* *
* @param image The source {@link BufferedImage} * @param image The source {@link BufferedImage}
* @param flipY Whether the image should be flipped vertically (for OpenGL uses) * @param flipY Whether the image should be flipped vertically (for OpenGL uses)
@ -147,7 +147,7 @@ public class ImageLoader
DataBuffer dataBuffer = data.getDataBuffer(); DataBuffer dataBuffer = data.getDataBuffer();
DataBufferByte byteBuffer = (DataBufferByte) dataBuffer; DataBufferByte byteBuffer = (DataBufferByte) dataBuffer;
byte[] byteData = byteBuffer.getData(); byte[] byteData = byteBuffer.getData();
ByteBuffer buffer = ByteBuffer.allocateDirect(width * height * 4).order(ByteOrder.nativeOrder()); ByteBuffer buffer = BufferUtils.createByteBuffer(width * height * 4);
buffer.put(byteData); buffer.put(byteData);
buffer.flip(); buffer.flip();
@ -156,9 +156,9 @@ public class ImageLoader
/** /**
* Writes a {@link BufferedImage} into a {@link ImageABGR} buffer. * Writes a {@link BufferedImage} into an {@link ImageABGR} buffer.
* *
* If the input {@link Path} is null, a placeholder will be generated. * If the input {@link BufferedImage} is null, a placeholder will be generated.
* *
* @param image The source {@link BufferedImage} * @param image The source {@link BufferedImage}
* *
@ -172,4 +172,75 @@ public class ImageLoader
{ {
return loadImageSpecial(image, true); return loadImageSpecial(image, true);
} }
/**
* Writes a {@link BufferedImage} into an {@link ImageY} buffer.
*
* If the input {@link Path} is null, a placeholder will be generated.
*
* @param image The source {@link BufferedImage}
* @param flipY Whether the image should be flipped vertically (for OpenGL uses)
*
* @return The output {@link ImageY}, never null
*
* @see ImageY
*
* @since 22.1.0.0-alpha.1
* */
public static ImageY loadImageGrayscaleSpecial(@Nullable BufferedImage image, boolean flipY)
{
if (image == null)
{
Logger.log(SmartSeverity.WARNING, "[TPL] Null BufferedImage supplied, generating a placeholder.");
return loadImageGrayscaleSpecial(placeholder, flipY);
}
int width = image.getWidth();
int height = image.getHeight();
if (width > 16384 || height > 16384 || width < 1 || height < 1)
{
Logger.log(SmartSeverity.ERROR, "[TPL] BufferedImage size is invalid (< 1 or > 16384), generating a placeholder.");
return loadImageGrayscaleSpecial(placeholder, flipY);
}
BufferedImage copy = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
Graphics2D imgGraphics = copy.createGraphics();
imgGraphics.drawImage(image,
0, flipY ? copy.getHeight() : 0, copy.getWidth(), flipY ? 0 : copy.getHeight(),
0, 0, image.getWidth(), image.getHeight(),
null); // I wonder if this is pixel-perfect
imgGraphics.dispose();
Raster data = copy.getRaster();
DataBuffer dataBuffer = data.getDataBuffer();
DataBufferByte byteBuffer = (DataBufferByte) dataBuffer;
byte[] byteData = byteBuffer.getData();
ByteBuffer buffer = BufferUtils.createByteBuffer(width * height);
buffer.put(byteData);
buffer.flip();
return new ImageY(buffer, width, height);
}
/**
* Writes a {@link BufferedImage} into an {@link ImageY} buffer.
*
* If the input {@link BufferedImage} is null, a placeholder will be generated.
*
* @param image The source {@link BufferedImage}
*
* @return The output {@link ImageABGR}, never null
*
* @see ImageY
*
* @since 22.1.0.0-alpha.1
* */
public static ImageY loadImageGrayscale(@Nullable BufferedImage image)
{
return loadImageGrayscaleSpecial(image, true);
}
} }

View File

@ -0,0 +1,71 @@
package org.plutoengine.tpl;
import java.awt.image.BufferedImage;
import java.nio.ByteBuffer;
/**
* A wrapper around a native Y buffer for easier handling
* by various APIs, such as OpenGL and GLFW.
*
* @author 493msi
*
* @since 22.1.0.0-alpha.0
*/
public class ImageY
{
private final ByteBuffer data;
private final int width;
private final int height;
/**
* Creates a new {@link ImageY} from the specified buffer, width and height.
*
* @param bfr The input {@link ByteBuffer}
* @param width This image's width
* @param height This image's height
*
* @since pre-alpha
* */
public ImageY(ByteBuffer bfr, int width, int height)
{
this.data = bfr;
this.width = width;
this.height = height;
}
/**
* Returns the width of the color buffer.
*
* @return The width of this {@link ImageY}
*
* @since pre-alpha
* */
public int getWidth()
{
return this.width;
}
/**
* Returns the height of the color buffer.
*
* @return The height of this {@link ImageY}
*
* @since pre-alpha
* */
public int getHeight()
{
return this.height;
}
/**
* Returns a read-only view of the color buffer.
*
* @return This image's color {@link ByteBuffer}
*
* @since pre-alpha
* */
public ByteBuffer getData()
{
return this.data.asReadOnlyBuffer();
}
}

View File

@ -3,7 +3,7 @@ package org.plutoengine.shader;
import org.lwjgl.opengl.GL33; import org.lwjgl.opengl.GL33;
import org.plutoengine.shader.type.IShader; import org.plutoengine.shader.type.IShader;
public interface IShaderProgram public interface IShaderProgram extends AutoCloseable
{ {
int getID(); int getID();
@ -27,5 +27,9 @@ public interface IShaderProgram
GL33.glUseProgram(0); GL33.glUseProgram(0);
} }
void dispose(); default void close()
{
this.stop();
GL33.glDeleteProgram(this.getID());
}
} }

View File

@ -127,8 +127,8 @@ public class RenderShaderBuilder
if (this.tempShaders) if (this.tempShaders)
{ {
this.vertexShader.dispose(); this.vertexShader.close();
this.fragmentShader.dispose(); this.fragmentShader.close();
} }
return null; return null;
@ -146,8 +146,8 @@ public class RenderShaderBuilder
if (this.tempShaders) if (this.tempShaders)
{ {
this.vertexShader.dispose(); this.vertexShader.close();
this.fragmentShader.dispose(); this.fragmentShader.close();
} }
return null; return null;
@ -158,8 +158,8 @@ public class RenderShaderBuilder
if (this.tempShaders) if (this.tempShaders)
{ {
this.vertexShader.dispose(); this.vertexShader.close();
this.fragmentShader.dispose(); this.fragmentShader.close();
} }
for (var field : fields) for (var field : fields)
@ -214,8 +214,13 @@ public class RenderShaderBuilder
{ {
AutomaticUniforms.VIEWPORT_PROJECTION.addListener(mat4 -> AutomaticUniforms.VIEWPORT_PROJECTION.addListener(mat4 ->
{ {
if (program.getID() == 0)
return false;
program.start(); program.start();
umat4.load(mat4); umat4.load(mat4);
return true;
}); });
Logger.logf(SmartSeverity.ADDED, "Uniform '%s' ID %d in '%s' ID %d now listens to AutomaticUniforms.VIEWPORT_PROJECTION.\n", uniformName, location, shaderClass.getCanonicalName(), programID); Logger.logf(SmartSeverity.ADDED, "Uniform '%s' ID %d in '%s' ID %d now listens to AutomaticUniforms.VIEWPORT_PROJECTION.\n", uniformName, location, shaderClass.getCanonicalName(), programID);
} }

View File

@ -15,12 +15,13 @@ public abstract class ShaderBase implements IShaderProgram
} }
@Override @Override
public void dispose() public void close()
{ {
Logger.logf(SmartSeverity.REMOVED, "Disposing of shader ID %d of type %s...\n", this.getID(), this.getClass().getCanonicalName()); Logger.logf(SmartSeverity.REMOVED, "Disposing of shader ID %d of type %s...\n", this.getID(), this.getClass().getCanonicalName());
this.stop(); IShaderProgram.super.close();
GL33.glDeleteProgram(this.programID);
this.programID = 0;
} }
protected void bindAttribute(int attribute, String name) protected void bindAttribute(int attribute, String name)

View File

@ -2,11 +2,11 @@ package org.plutoengine.shader.type;
import org.lwjgl.opengl.GL33; import org.lwjgl.opengl.GL33;
public interface IShader public interface IShader extends AutoCloseable
{ {
int getID(); int getID();
default void dispose() default void close()
{ {
GL33.glDeleteShader(this.getID()); GL33.glDeleteShader(this.getID());
} }

View File

@ -53,9 +53,9 @@ public class PlutoSpriteSheetMod implements IModEntryPoint
{ {
FramebufferTiledSpriteSheet.setSpriteShader(null); FramebufferTiledSpriteSheet.setSpriteShader(null);
spriteSheetShader.dispose(); spriteSheetShader.close();
shaderRectangle2D.dispose(); shaderRectangle2D.close();
shader2D.dispose(); shader2D.close();
RectangleRenderer2D.unload(); RectangleRenderer2D.unload();
Renderer2D.unload(); Renderer2D.unload();

View File

@ -58,12 +58,12 @@ public class RectangleRenderer2D
{ {
if (standardQuad != null) if (standardQuad != null)
{ {
standardQuad.delete(); standardQuad.close();
} }
if (centeredQuad != null) if (centeredQuad != null)
{ {
centeredQuad.delete(); centeredQuad.close();
} }
} }

View File

@ -51,12 +51,12 @@ public class Renderer2D
{ {
if (standardQuad != null) if (standardQuad != null)
{ {
standardQuad.delete(); standardQuad.close();
} }
if (centeredQuad != null) if (centeredQuad != null)
{ {
centeredQuad.delete(); centeredQuad.close();
} }
} }

View File

@ -12,7 +12,7 @@ public class DisposableTextureSprite extends PartialTextureSprite implements Spr
@Override @Override
public void delete() public void delete()
{ {
this.spriteTexture.delete(); this.spriteTexture.close();
this.spriteTexture = null; this.spriteTexture = null;
} }
} }

View File

@ -69,7 +69,7 @@ public class FramebufferTiledSpriteSheet extends TiledSpriteSheet<RectangleTextu
} }
this.spriteFBO.unbind(); this.spriteFBO.unbind();
this.spriteSheet.delete(); this.spriteSheet.close();
this.spriteSheet = newSpriteSheet; this.spriteSheet = newSpriteSheet;
} }
@ -88,7 +88,7 @@ public class FramebufferTiledSpriteSheet extends TiledSpriteSheet<RectangleTextu
this.sampler.delete(); this.sampler.delete();
this.sampler = null; this.sampler = null;
this.spriteSheet.delete(); this.spriteSheet.close();
// this.spriteSheet = null; // this.spriteSheet = null;
// Done by parent // Done by parent

View File

@ -13,7 +13,7 @@ import org.plutoengine.logger.SmartSeverity;
import org.plutoengine.tpl.ImageABGR; import org.plutoengine.tpl.ImageABGR;
import org.plutoengine.tpl.ImageLoader; import org.plutoengine.tpl.ImageLoader;
public abstract class Texture public abstract class Texture implements AutoCloseable
{ {
protected int glID; protected int glID;
protected final int type; protected final int type;
@ -49,7 +49,7 @@ public abstract class Texture
GL33.glBindTexture(this.type, 0); GL33.glBindTexture(this.type, 0);
} }
public void delete() public void close()
{ {
Logger.logf(SmartSeverity.REMOVED, "Texture with ID %d of type '%s' deleted...\n", this.glID, this.getClass().getSimpleName()); Logger.logf(SmartSeverity.REMOVED, "Texture with ID %d of type '%s' deleted...\n", this.glID, this.getClass().getSimpleName());

View File

@ -6,6 +6,10 @@
"icons": { "icons": {
"path": "icons", "path": "icons",
"type": "open" "type": "open"
},
"fonts": {
"path": "fonts.zip",
"type": "zip"
} }
} }
} }

View File

@ -1,15 +1,26 @@
#version 330 core #version 330 core
in vec2 uvCoordinates; in vec2 uvCoordinates;
flat in int atlasPage;
uniform sampler2DRect textureSampler; uniform sampler2DArray textureSampler;
uniform vec4 recolor; uniform vec4 recolor;
out vec4 out_Color; out vec4 out_Color;
void main(void) void main(void)
{ {
vec4 color = texture(textureSampler, uvCoordinates); vec3 textCoords = vec3(uvCoordinates, atlasPage);
out_Color = color * recolor; float threshold = 1 - 240.0 / 255.0;
float signedDist = 1 - texture(textureSampler, textCoords).r;
float fw = fwidth(signedDist);
vec4 col = recolor;
col.a *= smoothstep(threshold + 0.2, threshold, signedDist);
out_Color = col;
} }

View File

@ -2,26 +2,21 @@
in vec2 position; in vec2 position;
in vec2 uvCoords; in vec2 uvCoords;
in int page;
out vec2 uvCoordinates; out vec2 uvCoordinates;
flat out int atlasPage;
uniform mat4 projection; uniform mat4 projection;
uniform mat4 transformation; uniform mat3 transformation;
uniform vec2 uvBase;
uniform vec2 uvDelta;
uniform int italic; uniform int italic;
void main(void) void main(void)
{ {
vec2 pos = vec2(position.x, position.y); vec2 pos = vec2(position.x - position.y * italic / 4.0, position.y);
atlasPage = page;
if(italic == 1) uvCoordinates = uvCoords;
{ vec3 transformed = vec3((transformation * vec3(pos, 1.0)).xy, 0.0);
pos = vec2(position.x - position.y / 4.0, position.y); gl_Position = projection * vec4(transformed, 1.0);
}
uvCoordinates = uvBase + uvCoords * uvDelta;
gl_Position = projection * transformation * vec4(pos, 0.0, 1.0);
} }

View File

@ -1,6 +1,9 @@
package org.plutoengine.demo; package org.plutoengine.demo;
import org.plutoengine.graphics.PlutoGUIMod; import org.plutoengine.graphics.PlutoGUIMod;
import org.plutoengine.graphics.gui.STBTTFont;
import org.plutoengine.mod.IModEntryPoint;
import org.plutoengine.mod.Mod;
import org.plutoengine.mod.ModEntry; import org.plutoengine.mod.ModEntry;
@ModEntry( @ModEntry(
@ -8,6 +11,19 @@ import org.plutoengine.mod.ModEntry;
version = VersionInfo.GAME_VERSION, version = VersionInfo.GAME_VERSION,
dependencies = { PlutoGUIMod.class } dependencies = { PlutoGUIMod.class }
) )
public class BasicApplicationDemoMod public class BasicApplicationDemoMod implements IModEntryPoint
{ {
public static STBTTFont font;
@Override
public void onLoad(Mod mod)
{
font = STBTTFont.load(mod.getResource("fonts$robotoregular#ttf"));
}
@Override
public void onUnload()
{
font.close();
}
} }

View File

@ -1,17 +1,29 @@
package org.plutoengine.demo; package org.plutoengine.demo;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL33; import org.lwjgl.opengl.GL33;
import org.plutoengine.PlutoApplication; import org.plutoengine.PlutoApplication;
import org.plutoengine.PlutoLocal;
import org.plutoengine.display.Display; import org.plutoengine.display.Display;
import org.plutoengine.display.Framerate; import org.plutoengine.graphics.PlutoGUIMod;
import org.plutoengine.gui.font.FontHelper; import org.plutoengine.graphics.Renderer2D;
import org.plutoengine.gui.font.FontRenderer; import org.plutoengine.graphics.TestFontRenderer;
import org.plutoengine.graphics.gl.vao.QuadPresets;
import org.plutoengine.graphics.gui.FontShader;
import org.plutoengine.graphics.texture.MagFilter;
import org.plutoengine.graphics.texture.MinFilter;
import org.plutoengine.graphics.texture.WrapMode;
import org.plutoengine.graphics.texture.texture2d.Texture2D;
import org.plutoengine.input.InputBus;
import org.plutoengine.math.ProjectionMatrix; import org.plutoengine.math.ProjectionMatrix;
import org.plutoengine.mod.ModLoader; import org.plutoengine.shader.RenderShaderBuilder;
import org.plutoengine.shader.uniform.auto.AutomaticUniforms; import org.plutoengine.shader.uniform.auto.AutomaticUniforms;
import org.plutoengine.tpl.ImageLoader;
import java.awt.image.BufferedImage;
import java.nio.file.Path;
import java.util.List;
public class Main extends PlutoApplication public class Main extends PlutoApplication
{ {
@ -20,6 +32,7 @@ public class Main extends PlutoApplication
public static void main(String[] args) throws Exception public static void main(String[] args) throws Exception
{ {
var config = new PlutoApplication.StartupConfig(); var config = new PlutoApplication.StartupConfig();
config.vsync(1);
config.windowName("JSRClone " + VersionInfo.GAME_VERSION); config.windowName("JSRClone " + VersionInfo.GAME_VERSION);
INSTANCE = new Main(); INSTANCE = new Main();
@ -35,6 +48,82 @@ public class Main extends PlutoApplication
var projection = ProjectionMatrix.createOrtho2D(this.display.getWidth(), this.display.getHeight()); var projection = ProjectionMatrix.createOrtho2D(this.display.getWidth(), this.display.getHeight());
AutomaticUniforms.VIEWPORT_PROJECTION.fire(projection); AutomaticUniforms.VIEWPORT_PROJECTION.fire(projection);
TestFontRenderer.drawString(BasicApplicationDemoMod.font, "Testing ");
if (InputBus.Keyboard.pressed(GLFW.GLFW_KEY_R))
{
var shader = PlutoGUIMod.fontShader;
var newShader = new RenderShaderBuilder(
PlutoGUIMod.instance.getResource("shaders.VertexFontShader#glsl"),
PlutoGUIMod.instance.getResource("shaders.FragmentFontShader#glsl")
).build(FontShader.class, false);
if (newShader != null)
{
shader.stop();
shader.close();
PlutoGUIMod.fontShader = newShader;
newShader.start();
newShader.recolor.load(1, 1, 1, 1);
}
}
/*
var va = Renderer2D.standardQuad;
va.bind();
va.enableAllAttributes();
var font = BasicApplicationDemoMod.font;
var tex = font.getAtlas();
tex.bind();
var shader = PlutoGUIMod.fontShader;
shader.start();
shader.recolor.load(1, 1, 1, 1);
shader.italic.load(false);
shader.page.load(0);
shader.uvBase.load(0.0f, 0.0f);
shader.uvDelta.load(1.0f, 1.0f);
for (int i = 0; i < tex.getDepth(); i++)
{
var padding = 8;
var x = i % 3;
var y = i / 3;
var size = this.size;
shader.transformationMatrix.load(TransformationMatrix.create(20 + x * (size + padding), 20 + y * (size + padding), 0,
0, 0, 0,
size, size, 1));
shader.page.load(i);
va.draw(DrawMode.TRIANGLES);
}
this.size += InputBus.Mouse.getScrollY() * 20;
if (InputBus.Keyboard.pressed(GLFW.GLFW_KEY_R))
{
var newShader = new RenderShaderBuilder(
PlutoGUIMod.instance.getResource("shaders.VertexFontShader#glsl"),
PlutoGUIMod.instance.getResource("shaders.FragmentFontShader#glsl")
).build(FontShader.class, false);
if (newShader != null)
{
shader.stop();
shader.close();
PlutoGUIMod.fontShader = newShader;
}
}
*/
/*
var buildStr = String.format("Build %s", VersionInfo.GAME_BUILD); var buildStr = String.format("Build %s", VersionInfo.GAME_BUILD);
var strWidth = FontHelper.calcStringWidth(buildStr, "default", 0.75f); var strWidth = FontHelper.calcStringWidth(buildStr, "default", 0.75f);
FontRenderer.drawString(this.display.getWidth() - strWidth + 1, 3, buildStr, 0, 0, 0, 1, 0.75f, true); FontRenderer.drawString(this.display.getWidth() - strWidth + 1, 3, buildStr, 0, 0, 0, 1, 0.75f, true);
@ -57,6 +146,8 @@ public class Main extends PlutoApplication
modNr++; modNr++;
} }
*/
} }
public static Display getDisplay() public static Display getDisplay()

1
libra Submodule

@ -0,0 +1 @@
Subproject commit 7e3c146c9a70c2ec24ded435021f279a757c7aff

View File

@ -2,7 +2,8 @@ rootProject.name = "plutoengine-sdk"
include("plutoengine", include("plutoengine",
"plutoengine-ext", "plutoengine-ext",
"plutoengine-demos") "plutoengine-demos",
"libra")
project(":plutoengine").projectDir = file("./engine-core") project(":plutoengine").projectDir = file("./engine-core")
project(":plutoengine-ext").projectDir = file("./engine-ext") project(":plutoengine-ext").projectDir = file("./engine-ext")