diff --git a/README.md b/README.md index f28ade3..fb7bef7 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,9 @@ Book # https://ahbejarano.gitbook.io/lwjglgamedev | Space | Move up | | Shift | Move down | | 0 | Destroy the highlighted block | +| 1 | Place a grass block | +| 2 | Place a cobble block | +| 3 | Place a glass block | | Ctrl | Faster (forwards and backwards only) | | Alt+F4 | Exit game | | Esc | Exit fullscreen and display mouse | diff --git a/res/cobbleblock.png b/res/cobbleblock.png index 7afd1fa..98c9359 100644 Binary files a/res/cobbleblock.png and b/res/cobbleblock.png differ diff --git a/res/cube-fblr,u,d.obj b/res/cube-fblr,u,d.obj new file mode 100644 index 0000000..86b74cb --- /dev/null +++ b/res/cube-fblr,u,d.obj @@ -0,0 +1,94 @@ +# vertices : cube +# 3---7 +# |\ \ +# 1 4-5-8 +# \| | +# 2---6 +v -1 -1 -1 +v -1 -1 1 +v -1 1 -1 +v -1 1 1 +v 1 -1 -1 +v 1 -1 1 +v 1 1 -1 +v 1 1 1 +# 9-16 (1-8) +v -1 -1 -1 +v -1 -1 1 +v -1 1 -1 +v -1 1 1 +v 1 -1 -1 +v 1 -1 1 +v 1 1 -1 +v 1 1 1 +# 17-24 (1-8) +v -1 -1 -1 +v -1 -1 1 +v -1 1 -1 +v -1 1 1 +v 1 -1 -1 +v 1 -1 1 +v 1 1 -1 +v 1 1 1 + +# texture +# side bottom +# top +# 3 6 9 +# 2 5 8 +# 1 4 7 +vt 0.0 0.0 +vt 0.0 0.5 +vt 0.0 1.0 +vt 0.5 0.0 +vt 0.5 0.5 +vt 0.5 1.0 +vt 1.0 0.0 +vt 1.0 0.5 +vt 1.0 1.0 + +# normals : -z z -y y -x x +# back front down up left right +vn 0 0 -1 +vn 0 0 1 +vn 0 -1 0 +vn 0 1 0 +vn -1 0 0 +vn 1 0 0 + +# faces : back front down up left right +# back +# 7 3 +# 5 1 +f 7/3/1 5/2/1 1/5/1 +f 7/3/1 1/5/1 3/6/1 + +# front +# 4 8 +# 2 6 +f 4/3/2 2/2/2 6/5/2 +f 4/3/2 6/5/2 8/6/2 + +# down +# 2 6 +# 1 5 +f 10/6/3 9/5/3 13/8/3 +f 10/6/3 13/8/3 14/9/3 + +# up +# 3 7 +# 4 8 +f 11/2/4 12/1/4 16/4/4 +f 11/2/4 16/4/4 15/5/4 + +# left +# 3 4 +# 1 2 +f 19/3/5 17/2/5 18/5/5 +f 19/3/5 18/5/5 20/6/5 + +# right +# 8 7 +# 6 5 +f 24/3/6 22/2/6 21/5/6 +f 24/3/6 21/5/6 23/6/6 diff --git a/res/cube-fblrud.obj b/res/cube-fblrud.obj new file mode 100644 index 0000000..1a7632d --- /dev/null +++ b/res/cube-fblrud.obj @@ -0,0 +1,86 @@ +# vertices : cube +# 3---7 +# |\ \ +# 1 4-5-8 +# \| | +# 2---6 +v -1 -1 -1 +v -1 -1 1 +v -1 1 -1 +v -1 1 1 +v 1 -1 -1 +v 1 -1 1 +v 1 1 -1 +v 1 1 1 +# 9-16 (1-8) +v -1 -1 -1 +v -1 -1 1 +v -1 1 -1 +v -1 1 1 +v 1 -1 -1 +v 1 -1 1 +v 1 1 -1 +v 1 1 1 +# 17-24 (1-8) +v -1 -1 -1 +v -1 -1 1 +v -1 1 -1 +v -1 1 1 +v 1 -1 -1 +v 1 -1 1 +v 1 1 -1 +v 1 1 1 + +# texture +# 2 4 +# 1 3 +vt 0.0 0.0 +vt 0.0 1.0 +vt 1.0 0.0 +vt 1.0 1.0 + +# normals : -z z -y y -x x +# back front down up left right +vn 0 0 -1 +vn 0 0 1 +vn 0 -1 0 +vn 0 1 0 +vn -1 0 0 +vn 1 0 0 + +# faces : back front down up left right +# back +# 7 3 +# 5 1 +f 7/2/1 5/1/1 1/3/1 +f 7/2/1 1/3/1 3/4/1 + +# front +# 4 8 +# 2 6 +f 4/2/2 2/1/2 6/3/2 +f 4/2/2 6/3/2 8/4/2 + +# down +# 2 6 +# 1 5 +f 10/2/3 9/1/3 13/3/3 +f 10/2/3 13/3/3 14/4/3 + +# up +# 3 7 +# 4 8 +f 11/2/4 12/1/4 16/3/4 +f 11/2/4 16/3/4 15/4/4 + +# left +# 3 4 +# 1 2 +f 19/2/5 17/1/5 18/3/5 +f 19/2/5 18/3/5 20/4/5 + +# right +# 8 7 +# 6 5 +f 24/2/6 22/1/6 21/3/6 +f 24/2/6 21/3/6 23/4/6 diff --git a/res/cube.obj b/res/cube.obj deleted file mode 100644 index 8a6eee0..0000000 --- a/res/cube.obj +++ /dev/null @@ -1,55 +0,0 @@ -v 1.0 -1.0 -1.0 -v 1.0 -1.0 1.0 -v -1.0 -1.0 1.0 -v -1.0 -1.0 -1.0 -v 1.0 1.0 -1.0 -v 1.0 1.0 1.0 -v -1.0 1.0 1.0 -v -1.0 1.0 -1.0 -v 1.0 -1.0 -1.0 -v 1.0 -1.0 -1.0 -v 1.0 -1.0 1.0 -v 1.0 -1.0 1.0 -v -1.0 -1.0 -1.0 -v -1.0 -1.0 -1.0 -v 1.0 1.0 -1.0 -v 1.0 1.0 -1.0 -v -1.0 -1.0 1.0 -v -1.0 -1.0 1.0 -v 1.0 1.0 1.0 -v 1.0 1.0 1.0 -v -1.0 1.0 1.0 -v -1.0 1.0 1.0 -v -1.0 1.0 -1.0 -v -1.0 1.0 -1.0 - -vt 0.5 1.0 -vt 0.5 0.5 -vt 1.0 0.5 -vt 0.0 0.5 -vt 0.0 0.0 -vt 0.5 0.0 -vt 0.0 1.0 -vt 0.0 1.0 -vt 0.5 1.0 -vt 1.0 1.0 - -vn 0.0 -1.0 0.0 -vn 0.0 1.0 0.0 -vn 1.0 0.0 0.0 -vn 0.0 0.0 1.0 -vn -1.0 0.0 0.0 -vn 0.0 0.0 -1.0 - -f 11/1/1 17/2/1 13/3/1 -f 24/4/2 22/5/2 20/6/2 -f 15/1/3 19/7/3 12/4/3 -f 6/1/4 21/8/4 18/4/4 -f 3/2/5 7/9/5 23/7/5 -f 1/4/6 4/2/6 8/1/6 -f 9/10/1 11/1/1 13/3/1 -f 16/2/2 24/4/2 20/6/2 -f 10/2/3 15/1/3 12/4/3 -f 2/2/4 6/1/4 18/4/4 -f 14/4/5 3/2/5 23/7/5 -f 5/7/6 1/4/6 8/1/6 diff --git a/res/fragment-3d-block.fs b/res/fragment-3d-block.fs new file mode 100644 index 0000000..92d0c79 --- /dev/null +++ b/res/fragment-3d-block.fs @@ -0,0 +1,20 @@ +#version 130 + +in vec2 outCoord; +out vec4 fragColor; + +uniform sampler2D texture_sampler; +uniform vec4 color; +uniform int isTextured; +uniform int isSelected; + +void main() { + if (isTextured > 0) { + fragColor = texture(texture_sampler, outCoord); + } else { + fragColor = color; + } + if (isSelected > 0) { + fragColor = vec4(fragColor.x, fragColor.y, 1, 1); + } +} diff --git a/res/fragment-3d.fs b/res/fragment-3d.fs index 77d71d1..faf3bbc 100644 --- a/res/fragment-3d.fs +++ b/res/fragment-3d.fs @@ -6,7 +6,6 @@ out vec4 fragColor; uniform sampler2D texture_sampler; uniform vec4 color; uniform int isTextured; -uniform int isSelected; void main() { if (isTextured > 0) { @@ -14,7 +13,4 @@ void main() { } else { fragColor = color; } - if (isSelected > 0) { - fragColor = vec4(fragColor.x, fragColor.y, 11, 1); - } } diff --git a/res/glassblock.png b/res/glassblock.png new file mode 100644 index 0000000..f184a13 Binary files /dev/null and b/res/glassblock.png differ diff --git a/res/skybox.obj b/res/skybox.obj index afc5063..1147484 100644 --- a/res/skybox.obj +++ b/res/skybox.obj @@ -1,86 +1,86 @@ # Front Face -v -1.0 1.0 1.0 -v -1.0 -1.0 1.0 -v 1.0 -1.0 1.0 -v 1.0 1.0 1.0 +v -1 1 1 +v -1 -1 1 +v 1 -1 1 +v 1 1 1 # Left Face -v -1.0 1.0 1.0 -v -1.0 -1.0 1.0 -v -1.0 -1.0 -1.0 -v -1.0 1.0 -1.0 +v -1 1 1 +v -1 -1 1 +v -1 -1 -1 +v -1 1 -1 # Right Face -v 1.0 1.0 1.0 -v 1.0 -1.0 1.0 -v 1.0 -1.0 -1.0 -v 1.0 1.0 -1.0 +v 1 1 1 +v 1 -1 1 +v 1 -1 -1 +v 1 1 -1 # Back Face -v -1.0 1.0 -1.0 -v -1.0 -1.0 -1.0 -v 1.0 -1.0 -1.0 -v 1.0 1.0 -1.0 +v -1 1 -1 +v -1 -1 -1 +v 1 -1 -1 +v 1 1 -1 # Top Face -v -1.0 1.0 -1.0 -v -1.0 1.0 1.0 -v 1.0 1.0 1.0 -v 1.0 1.0 -1.0 +v -1 1 -1 +v -1 1 1 +v 1 1 1 +v 1 1 -1 # Bottom Face -v -1.0 -1.0 -1.0 -v -1.0 -1.0 1.0 -v 1.0 -1.0 1.0 -v 1.0 -1.0 -1.0 +v -1 -1 -1 +v -1 -1 1 +v 1 -1 1 +v 1 -1 -1 # Front Face vt 0.333333 0.5 -vt 0.333333 0.0 -vt 0.0 0.0 -vt 0.0 0.5 +vt 0.333333 0 +vt 0 0 +vt 0 0.5 # Left Face -vt 0.0 1.0 -vt 0.0 0.5 +vt 0 1 +vt 0 0.5 vt 0.333333 0.5 -vt 0.333333 1.0 +vt 0.333333 1 # Right Face -vt 1.0 1.0 -vt 1.0 0.5 +vt 1 1 +vt 1 0.5 vt 0.666666 0.5 -vt 0.666666 1.0 +vt 0.666666 1 # Back Face -vt 0.333333 1.0 +vt 0.333333 1 vt 0.333333 0.5 vt 0.666666 0.5 -vt 0.666666 1.0 +vt 0.666666 1 # Top Face -vt 0.335 0.5 +vt 0.335 0.5 vt 0.666666 0.5 -vt 0.666666 0.0 -vt 0.335 0.0 +vt 0.666666 0 +vt 0.335 0 # Bottom Face vt 0.666666 0.5 -vt 0.666666 0.0 -vt 1.0 0.0 -vt 1.0 0.5 +vt 0.666666 0 +vt 1 0 +vt 1 0.5 # Front Face -f 1/1/ 2/2/ 3/3/ -f 4/4/ 1/1/ 3/3/ +f 1/1/ 2/2/ 3/3/ +f 4/4/ 1/1/ 3/3/ # Left Face -f 5/5/ 6/6/ 7/7/ -f 8/8/ 5/5/ 7/7/ +f 5/5/ 6/6/ 7/7/ +f 8/8/ 5/5/ 7/7/ # Right Face -f 9/9/ 10/10/ 11/11/ -f 12/12/ 9/9/ 11/11/ +f 9/9/ 10/10/ 11/11/ +f 12/12/ 9/9/ 11/11/ # Back Face f 13/13/ 14/14/ 15/15/ diff --git a/src/Main.java b/src/Main.java index 5c88667..f0ead8d 100644 --- a/src/Main.java +++ b/src/Main.java @@ -12,13 +12,9 @@ public class Main { public static void main(String[] args) { try { - int mode = Window.WINDOWED; - boolean vSync = true; - int targetFps = 10; - int targetUps = 30; - Window window = new Window("Hello World!", 300, 300, mode, vSync, targetFps, targetUps); - Scene scene = new Game(); - Engine engine = new Engine(window, scene); + Window window = new Window("Hello World!", 300, 300, Window.WINDOWED, /*vSync*/ true, /*targetFps*/ 10); + Loopable loop = new Game(); + Engine engine = new Engine(window, loop); engine.run(); } catch (Exception e) { e.printStackTrace(); diff --git a/src/engine/Bucket.java b/src/engine/Bucket.java index 258b6bf..77491fa 100644 --- a/src/engine/Bucket.java +++ b/src/engine/Bucket.java @@ -10,7 +10,7 @@ Bucket class (map of lambdas). public class Bucket { public final Map next; public Bucket() { this.next = new HashMap<>(); } - + public boolean run(String string) { Runnable runnable = this.remove(string); if (runnable == null) @@ -22,6 +22,6 @@ public boolean run(String string) { public Runnable remove(String string) { return this.next.remove(string); } public boolean empty() { return this.next.isEmpty(); } public Bucket add(String string, Runnable runnable) { this.next.put(string, runnable); return this; } - + public String toString() { return this.next.toString(); } } diff --git a/src/engine/Camera.java b/src/engine/Camera.java index 130fa03..8de2161 100644 --- a/src/engine/Camera.java +++ b/src/engine/Camera.java @@ -5,56 +5,81 @@ package geetransit.minecraft05.engine; +import org.joml.Vector2f; import org.joml.Vector3f; import org.joml.Matrix4f; +import org.joml.FrustumIntersection; -public class Camera { +public class Camera implements Inputtable { public static final float FOV = 80f; public static final float NEAR = 0.01f; public static final float FAR = 50f; - + public static final float SENSITIVITY = 0.3f; + + private Mouse mouse; + private final Vector3f position; private final Vector3f rotation; // in degrees + private final Matrix4f viewMatrix; - + private final Matrix4f projViewMatrix; + private final FrustumIntersection frustumIntersection; + private float fov; private float near; private float far; - - private Vector3f radiansRotation; - private Vector3f negativePosition; + private float sensitivity; + + private final Vector3f radiansRotation; + private final Vector3f negativePosition; + + public Camera(Mouse mouse) { + this.mouse = mouse; + + this.position = new Vector3f(); + this.rotation = new Vector3f(); - public Camera(Vector3f position, Vector3f rotation, float fov, float near, float far) { - this.position = position; - this.rotation = rotation; this.viewMatrix = new Matrix4f(); - - this.fov = (float) Math.toRadians(fov); - this.near = near; - this.far = far; - + this.projViewMatrix = new Matrix4f(); + this.frustumIntersection = new FrustumIntersection(); + + this.setFov(FOV); + this.setNear(NEAR); + this.setFar(FAR); + this.setSensitivity(SENSITIVITY); + this.radiansRotation = new Vector3f(); this.negativePosition = new Vector3f(); } - public Camera(Vector3f position, Vector3f rotation, float fov) { this(position, rotation, fov, NEAR, FAR); } - public Camera(Vector3f position, Vector3f rotation) { this(position, rotation, FOV); } - public Camera() { this(new Vector3f(), new Vector3f()); } - + + @Override + public void input(Window window) { + if (this.mouse.isInside()) + this.rotateMovement(this.mouse.getMovement(), this.sensitivity); + } + + public Mouse getMouse() { return this.mouse; } public Vector3f getPosition() { return this.position; } public Vector3f getRotation() { return this.rotation; } - public float getFov() { return (float) Math.toDegrees(this.fov); } + public float getFov() { return (float) Math.toDegrees(this.fov); } public float getNear() { return this.near; } - public float getFar() { return this.far; } - public Camera setPosition(Vector3f position) { this.position.set(position); return this; } + public float getFar() { return this.far; } + public float getSensitivity() { return this.sensitivity; } + + public Camera setMouse(Mouse mouse) { this.mouse = mouse; return this; } + public Camera setPosition(Vector3f position) { this.position.set(position); return this; } public Camera setPosition(float x, float y, float z) { this.position.set(x, y, z); return this; } - public Camera setRotation(Vector3f rotation) { this.rotation.set(rotation); return this; } + public Camera setRotation(Vector3f rotation) { this.rotation.set(rotation); return this; } public Camera setRotation(float x, float y, float z) { this.rotation.set(x, y, z); return this; } - public Camera setFov(float fov) { this.fov = (float) Math.toRadians(fov); return this; } + public Camera setFov(float fov) { this.fov = (float) Math.toRadians(fov); return this; } public Camera setNear(float near) { this.near = near; return this; } - public Camera setFar(float far) { this.far = far; return this; } + public Camera setFar(float far) { this.far = far; return this; } + public Camera setSensitivity(float sensitivity) { this.sensitivity = sensitivity; return this; } public Camera movePosition(Vector3f position) { return this.movePosition(position.x, position.y, position.z); } + public Camera movePosition(Vector3f position, float step) { return this.movePosition(position.x*step, position.y*step, position.z*step); } public Camera movePosition(float x, float y, float z) { + // TODO optimize this (using functions inside Vector3f) if (z != 0) { this.position.x += (float) Math.sin(Math.toRadians(this.rotation.y)) * -1.0f * z; this.position.z += (float) Math.cos(Math.toRadians(this.rotation.y)) * z; @@ -66,16 +91,16 @@ public Camera movePosition(float x, float y, float z) { this.position.y += y; return this; } - + public Camera moveRotation(Vector3f rotation) { this.rotation.add(rotation); return this; } public Camera moveRotation(float x, float y, float z) { this.rotation.add(x, y, z); return this; } - - public Camera rotateUsingMouse(Mouse mouse, float sensitivity) { - float x = mouse.getMovement().x; - float y = mouse.getMovement().y; + + public Camera rotateMovement(Vector2f movement, float sensitivity) { + float x = movement.x; + float y = movement.y; if (mouse.isLeft()) // dragging this.moveRotation(-y*sensitivity, -x*sensitivity, 0); if (mouse.isRight()) // panning @@ -83,7 +108,7 @@ public Camera rotateUsingMouse(Mouse mouse, float sensitivity) { this.rotation.x = Math.max(-90f, Math.min(90f, this.rotation.x)); // don't allow neck snapping return this; } - + public Vector3f getRadiansRotation() { return this.radiansRotation.set( (float) Math.toRadians(this.rotation.x), @@ -91,18 +116,31 @@ public Vector3f getRadiansRotation() { (float) Math.toRadians(this.rotation.z) ); } - + public Vector3f getNegativePosition() { return this.position.negate(this.negativePosition); } - - public Matrix4f getViewMatrix() { + + public Matrix4f getViewMatrix() { return this.viewMatrix; } + public Matrix4f buildViewMatrix() { return this.viewMatrix .identity() .rotateXYZ(this.getRadiansRotation()) .translate(this.getNegativePosition()); } - + + public void updateFrustum(Matrix4f projMatrix) { + projMatrix.mul(this.viewMatrix, this.projViewMatrix); + this.frustumIntersection.set(this.projViewMatrix); + } + + public boolean insideFrustum(Vector3f position, float radius) { + return this.insideFrustum(position.x, position.y, position.z, radius); + } + public boolean insideFrustum(float x, float y, float z, float radius) { + return this.frustumIntersection.testSphere(x, y, z, radius); + } + public String toString() { return String.format( "<%s position=%s rotation=%s>", diff --git a/src/engine/ClosestItem.java b/src/engine/ClosestItem.java index 5aa09cd..c21bd5b 100644 --- a/src/engine/ClosestItem.java +++ b/src/engine/ClosestItem.java @@ -11,52 +11,51 @@ import org.joml.Vector3f; import org.joml.Intersectionf; -public class ClosestItem { +public class ClosestItem { public float distance; // distance from camera - public Item closest; + public T closest; public Vector3f hit; // position of intersection public Vector3f direction; // points away from camera - + private final Vector3f max; private final Vector3f min; private final Vector2f nearFar; - + public ClosestItem() { this.hit = new Vector3f(); this.direction = new Vector3f(); - + this.min = new Vector3f(); this.max = new Vector3f(); this.nearFar = new Vector2f(); } - public ClosestItem(List items, Camera camera) { + public ClosestItem(Iterable items, Camera camera) { this(); this.update(items, camera); } - - public ClosestItem reset() { + + public ClosestItem reset() { this.distance = Float.POSITIVE_INFINITY; this.closest = null; return this; } - - public ClosestItem update(List items, Camera camera) { + + public void update(Iterable items, Camera camera) { this.reset().extend(items, camera); - return this; } - - public ClosestItem extend(List items, Camera camera) { + + public ClosestItem extend(Iterable items, Camera camera) { // get camera direction camera.getViewMatrix().positiveZ(this.direction); this.direction.negate().normalize(); - + // loop through all items - for (Item item : items) { + for (T item : items) { this.min.set(item.getPosition()); this.max.set(item.getPosition()); this.min.add(-item.getScale(), -item.getScale(), -item.getScale()); this.max.add(item.getScale(), item.getScale(), item.getScale()); - + // check if intersects and is closer if (Intersectionf.intersectRayAab( camera.getPosition(), this.direction, @@ -71,7 +70,7 @@ public ClosestItem extend(List items, Camera camera) { } } } - + // allow method chaining return this; } diff --git a/src/engine/Countdown.java b/src/engine/Countdown.java new file mode 100644 index 0000000..bf37c44 --- /dev/null +++ b/src/engine/Countdown.java @@ -0,0 +1,46 @@ +/* +George Zhang +Countdown class. +*/ + +package geetransit.minecraft05.engine; + +public class Countdown { + private float interval; + private float wait; + + public Countdown(float interval) { + this.interval = interval; + this.wait = 0f; + } + + public float getWait() { return this.wait; } + public float getInterval() { return this.interval; } + public Countdown setInterval(float interval) { this.interval = interval; return this; } + + public Countdown add(float time) { + this.wait -= time; + return this; + } + + public Countdown reset() { + this.wait = 0f; + return this; + } + + // while (countdown.next()) + public boolean next() { + if (this.wait > 0) + return false; + this.wait += this.interval; + return true; + } + + // if (countdown.nextOnce()) + public boolean nextOnce() { + if (this.wait > 0) + return false; + this.wait = this.interval; + return true; + } +} diff --git a/src/engine/Engine.java b/src/engine/Engine.java index f26e753..2a46277 100644 --- a/src/engine/Engine.java +++ b/src/engine/Engine.java @@ -7,27 +7,27 @@ public class Engine implements Runnable { private Window window; - private Scene scene; - + private Loopable loop; + private boolean updateVSync = false; private boolean updateSize = false; private boolean destroyed = false; - + public Engine( Window window, - Scene scene + Loopable loop ) { this.window = window; - this.scene = scene; + this.loop = loop; } - + @Override public void run() { this.window.createWindow(); - new Thread(() -> this.window.renderThread(this.scene)).start(); + new Thread(() -> this.window.renderThread(this.loop)).start(); this.window.eventThread(); } - + public Window getWindow() { return this.window; } - public Scene getScene() { return this.scene; } + public Loopable getLoop() { return this.loop; } } diff --git a/src/engine/FontTexture.java b/src/engine/FontTexture.java new file mode 100644 index 0000000..a70a0d4 --- /dev/null +++ b/src/engine/FontTexture.java @@ -0,0 +1,106 @@ +/* +George Zhang +Font texture subclass. +*/ + +package geetransit.minecraft05.engine; + +import java.util.*; +import java.nio.charset.Charset; + +public class FontTexture extends Texture { + public static final float ZPOS = 0f; + public static final int VERTICES_PER_QUAD = 4; + + private final int cols; + private final int rows; + + public FontTexture(String fileName, int cols, int rows) { + super(fileName); + this.cols = cols; + this.rows = rows; + } + + public int getCols() { return this.cols; } + public int getRows() { return this.rows; } + + public Mesh buildMesh(String text) { + byte[] charArray = text.getBytes(Charset.forName("ISO-8859-1")); + + List posList = new ArrayList<>(); + List coordList = new ArrayList<>(); + List indexList = new ArrayList<>(); + + int fontCols = this.getCols(); + int fontRows = this.getRows(); + float charWidth = (float) this.getWidth() / fontCols; + float charLength = (float) this.getLength() / fontRows; + + int currentCol = 0; + int currentRow = 0; + int currentIndex = 0; + for (int i = 0; i < charArray.length; i++) { + byte currentChar = charArray[i]; + if (currentChar == '\n') { + currentRow++; + currentCol = 0; + continue; + } + + // Build a character tile composed by two triangles + // 0 2 + // 1 3 + int fontCol = currentChar % fontCols; + int fontRow = currentChar / fontCols; + + // Left Top vertex + posList.add((currentCol + 0) * charWidth); // x + posList.add((currentRow + 0) * charLength); // y + posList.add(ZPOS); // z + coordList.add((float) (fontCol + 0) / fontCols); + coordList.add((float) (fontRow + 0) / fontRows); + + // Left Bottom vertex + posList.add((currentCol + 0) * charWidth); + posList.add((currentRow + 1) * charLength); + posList.add(ZPOS); + coordList.add((float) (fontCol + 0) / fontCols); + coordList.add((float) (fontRow + 1) / fontRows); + + // Right Top vertex + posList.add((currentCol + 1) * charWidth); + posList.add((currentRow + 0) * charLength); + posList.add(ZPOS); + coordList.add((float) (fontCol + 1) / fontCols); + coordList.add((float) (fontRow + 0) / fontRows); + + // Right Bottom vertex + posList.add((currentCol + 1) * charWidth); + posList.add((currentRow + 1) * charLength); + posList.add(ZPOS); + coordList.add((float) (fontCol + 1) / fontCols); + coordList.add((float) (fontRow + 1) / fontRows); + + // Add indices for triangles (counter-clockwise) + // 0 0 2 + // 1 3 3 + indexList.add(currentIndex*VERTICES_PER_QUAD + 0); + indexList.add(currentIndex*VERTICES_PER_QUAD + 1); + indexList.add(currentIndex*VERTICES_PER_QUAD + 3); + indexList.add(currentIndex*VERTICES_PER_QUAD + 0); + indexList.add(currentIndex*VERTICES_PER_QUAD + 3); + indexList.add(currentIndex*VERTICES_PER_QUAD + 2); + + currentCol++; + currentIndex++; + } + + float[] posArray = Utils.floatListToArray(posList); + float[] coordArray = Utils.floatListToArray(coordList); + int[] indexArray = Utils.intListToArray(indexList); + + Mesh mesh = new Mesh(posArray, indexArray, coordArray); + mesh.setTexture(this); + return mesh; + } +} diff --git a/src/engine/HeightMap.java b/src/engine/HeightMap.java deleted file mode 100644 index 95a37df..0000000 --- a/src/engine/HeightMap.java +++ /dev/null @@ -1,53 +0,0 @@ -/* -George Zhang -Encapsulate an image (used to get pixel values). -*/ - -package geetransit.minecraft05.engine; - -import java.nio.ByteBuffer; - -public class HeightMap implements AutoCloseable { - public static int CHANNELS = 4; - - public final ByteBuffer buffer; - public final int width; - public final int length; - - public HeightMap(ByteBuffer buffer, int width, int length) { - this.buffer = buffer; - this.width = width; - this.length = length; - } - - public static HeightMap loadFromImage(String fileName) throws Exception { - int width[] = {0}, length[] = {0}; - ByteBuffer buffer = Utils.loadImage(fileName, width, length); - return new HeightMap(buffer, width[0], length[0]); - } - - @Override - public void close() { - Utils.freeImage(this.buffer); - } - - // usage: compressExpand(heightAt(...), 0, MAX_COLOR, 0, 16) - public static float compressExpand(int f, float cMin, float cMax, float eMin, float eMax) { - return expand(compress(f, cMin, cMax), eMin, eMax); - } - public static float compress(int f, float min, float max) { return (f-min) / (max-min); } - public static float expand(float f, float min, float max) { return min + f*(max-min); } - - public int heightAt(int x, int z) { return this.heightAt(x*CHANNELS + z*CHANNELS*this.width); } - public int heightAt(int i) { - byte r = this.buffer.get(i + 0); - byte g = this.buffer.get(i + 1); - byte b = this.buffer.get(i + 2); - byte a = this.buffer.get(i + 3); - return 0 - // | ((0xFF & a) << 24) // removed cuz it turns overflows int - | ((0xFF & r) << 16) - | ((0xFF & g) << 8) - | ((0xFF & b) << 0); - } -} diff --git a/src/engine/Image.java b/src/engine/Image.java new file mode 100644 index 0000000..5ebde62 --- /dev/null +++ b/src/engine/Image.java @@ -0,0 +1,74 @@ +/* +George Zhang +Encapsulate an image (used to get pixel values). +*/ + +package geetransit.minecraft05.engine; + +import java.nio.ByteBuffer; +import java.nio.IntBuffer; + +import org.lwjgl.system.MemoryUtil; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.stb.STBImage; + +public class Image implements AutoCloseable { + public static final int CHANNELS = 4; + public static final int MAX = 0x00FFFFFF; + public static final int MAX_ALPHA = 0xFFFFFFFF; + + public final ByteBuffer buffer; + public final int width; + public final int length; + + public Image(String file) { this(Utils.loadByteArray(file)); } + public Image(byte[] array) { this((ByteBuffer) MemoryUtil.memAlloc(array.length).put(array).flip()); } + public Image(ByteBuffer raw) { + // Load Texture file + try (MemoryStack stack = MemoryStack.stackPush()) { + IntBuffer width = stack.mallocInt(1); + IntBuffer length = stack.mallocInt(1); + IntBuffer channels = stack.mallocInt(1); + + this.buffer = STBImage.stbi_load_from_memory(raw, width, length, channels, 4); + if (this.buffer == null) + throw new RuntimeException("Buffer could not be loaded: " + STBImage.stbi_failure_reason()); + + // store width and length of image + this.width = width.get(); + this.length = length.get(); + } + } + + @Override + public void close() { + STBImage.stbi_image_free(this.buffer); + } + + // usage: compressExpand(heightAt(...), 0, MAX_COLOR, 0, 16) + public static float compressExpand(int f, float cMin, float cMax, float eMin, float eMax) { + return expand(compress(f, cMin, cMax), eMin, eMax); + } + public static float compress(int f, float min, float max) { return (f-min) / (max-min); } + public static float expand(float f, float min, float max) { return min + f*(max-min); } + + // RRGGBB + public int pixel(int x, int z) { + int i = x*CHANNELS + z*CHANNELS*this.width; + return 0 + | (this.buffer.get(i + 0) & 0xFF) << 020 + | (this.buffer.get(i + 1) & 0xFF) << 010 + | (this.buffer.get(i + 2) & 0xFF) << 000; + } + + // RRGGBBAA + // remember to use >>> when shifting down + public int pixelAlpha(int x, int z) { + int i = x*CHANNELS + z*CHANNELS*this.width; + return 0 + | (this.buffer.get(i + 0) & 0xFF) << 030 + | (this.buffer.get(i + 1) & 0xFF) << 020 + | (this.buffer.get(i + 2) & 0xFF) << 010 + | (this.buffer.get(i + 3) & 0xFF) << 000; + } +} diff --git a/src/engine/Initializable.java b/src/engine/Initializable.java new file mode 100644 index 0000000..0078ed3 --- /dev/null +++ b/src/engine/Initializable.java @@ -0,0 +1,11 @@ +/* +George Zhang +Initializable interface. +*/ + +package geetransit.minecraft05.engine; + +public interface Initializable { + void init(Window window); + default void cleanup() {} +} diff --git a/src/engine/Inputtable.java b/src/engine/Inputtable.java new file mode 100644 index 0000000..8470835 --- /dev/null +++ b/src/engine/Inputtable.java @@ -0,0 +1,10 @@ +/* +George Zhang +Inputtable interface +*/ + +package geetransit.minecraft05.engine; + +public interface Inputtable { + void input(Window window); +} diff --git a/src/engine/Item.java b/src/engine/Item.java index 1a56449..d2e78f8 100644 --- a/src/engine/Item.java +++ b/src/engine/Item.java @@ -1,20 +1,19 @@ /* ahbejarano -Game item wrapper class. +Game item class. */ package geetransit.minecraft05.engine; import org.joml.Vector3f; import org.joml.Quaternionf; +import org.joml.Matrix4f; -public class Item implements Cloneable { - protected Mesh mesh; - +public class Item { + private Mesh mesh; private final Vector3f position; private final Quaternionf rotation; // Degrees, not radians. private float scale; - private boolean selected; public Item(Mesh mesh) { this(); @@ -24,31 +23,11 @@ protected Item() { this.position = new Vector3f(); this.rotation = new Quaternionf(); this.scale = 1; - this.selected = false; - } - - public void render(Window window) { - this.mesh.prepare(); - this.mesh.render(); - this.mesh.restore(); - } - - public void cleanup() { - this.mesh.cleanup(); - } - - // does NOT copy the mesh (shallow copy) - @Override - public Item clone() { - return new Item(this.getMesh()) - .setPosition(this.getPosition()) - .setRotation(this.getRotation()) - .setScale(this.getScale()) - .setSelected(this.isSelected()); } - + public Mesh getMesh() { return this.mesh; } - + protected Item setMesh(Mesh mesh) { this.mesh = mesh; return this; } + public Vector3f getPosition() { return this.position; } public Item setPosition(Vector3f position) { this.position.set(position); return this; } public Item setPosition(float x, float y, float z) { @@ -57,7 +36,7 @@ public Item setPosition(float x, float y, float z) { this.position.z = z; return this; } - + public Quaternionf getRotation() { return this.rotation; } public Item setRotation(Quaternionf rotation) { this.rotation.set(rotation); return this; } public Item setRotation(float x, float y, float z) { @@ -66,16 +45,32 @@ public Item setRotation(float x, float y, float z) { this.rotation.z = z; return this; } - + public float getScale() { return this.scale; } public Item setScale(float scale) { this.scale = scale; return this; } - - public boolean isSelected() { return this.selected; } - public Item setSelected(boolean selected) { - this.selected = selected; - return this; + + public Matrix4f buildModelMatrix(Matrix4f result) { + return result.translationRotateScale(this.position, this.rotation, this.scale); + } + + public Matrix4f buildModelViewMatrix(Matrix4f viewMatrix, Matrix4f result) { + return viewMatrix.mulAffine(this.buildModelMatrix(result), result); + } + + // these 2 are different? + public Matrix4f newBuildOrthoProjModelMatrix(Matrix4f orthoMatrix, Matrix4f result) { + return orthoMatrix.mulOrthoAffine(this.buildModelMatrix(result), result); + } + public Matrix4f buildOrthoProjModelMatrix(Matrix4f orthoMatrix, Matrix4f result) { + return result + .set(orthoMatrix) + .translate(this.position) + .rotateX((float) Math.toRadians(-this.rotation.x)) + .rotateY((float) Math.toRadians(-this.rotation.y)) + .rotateZ((float) Math.toRadians(-this.rotation.z)) + .scale(this.scale); } } diff --git a/src/engine/Loopable.java b/src/engine/Loopable.java new file mode 100644 index 0000000..1f0a5f7 --- /dev/null +++ b/src/engine/Loopable.java @@ -0,0 +1,20 @@ +/* +George Zhang +Loopable interface class. (used by Window) +*/ + +package geetransit.minecraft05.engine; + +public interface Loopable extends Initializable, Inputtable, Updateable, Renderable { + @Override + default void init(Window window) {} + + @Override + default void input(Window window) {} + + @Override + default void update(float interval) {} + + @Override + default void render(Window window) {} +} diff --git a/src/engine/Mesh.java b/src/engine/Mesh.java index 63ae02f..c353da2 100644 --- a/src/engine/Mesh.java +++ b/src/engine/Mesh.java @@ -6,18 +6,20 @@ package geetransit.minecraft05.engine; import java.util.*; +import java.util.function.*; +import java.util.stream.*; import java.nio.*; import org.joml.*; import static org.lwjgl.opengl.GL30.*; import static org.lwjgl.system.MemoryUtil.*; -public class Mesh { +public class Mesh implements AutoCloseable { private static final Vector3f DEFAULT_COLOUR = new Vector3f(0.0f, 0.0f, 0.0f); - + private final int vaoId; private final int vertexCount; private final List vboIdList; - + private Texture texture; private Vector4f color; @@ -26,7 +28,7 @@ public Mesh(float[] posArray, int[] indexArray, float[] coordArray) { this.vertexCount = indexArray.length; this.color = new Vector4f(); this.setColor(DEFAULT_COLOUR); - + int vboId; FloatBuffer posBuffer = null; IntBuffer indexBuffer = null; @@ -35,8 +37,7 @@ public Mesh(float[] posArray, int[] indexArray, float[] coordArray) { // Create the VAO this.vaoId = glGenVertexArrays(); glBindVertexArray(this.vaoId); - - + // Position VBO vboId = glGenBuffers(); this.vboIdList.add(vboId); @@ -44,9 +45,10 @@ public Mesh(float[] posArray, int[] indexArray, float[] coordArray) { posBuffer.put(posArray).flip(); glBindBuffer(GL_ARRAY_BUFFER, vboId); glBufferData(GL_ARRAY_BUFFER, posBuffer, GL_STATIC_DRAW); + // position uniform glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0); - + // Index VBO vboId = glGenBuffers(); this.vboIdList.add(vboId); @@ -54,7 +56,7 @@ public Mesh(float[] posArray, int[] indexArray, float[] coordArray) { indexBuffer.put(indexArray).flip(); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboId); glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexBuffer, GL_STATIC_DRAW); - + // texture coords VBO vboId = glGenBuffers(); this.vboIdList.add(vboId); @@ -62,13 +64,14 @@ public Mesh(float[] posArray, int[] indexArray, float[] coordArray) { coordBuffer.put(coordArray).flip(); glBindBuffer(GL_ARRAY_BUFFER, vboId); glBufferData(GL_ARRAY_BUFFER, coordBuffer, GL_STATIC_DRAW); + // coord uniform glEnableVertexAttribArray(1); glVertexAttribPointer(1, 2, GL_FLOAT, false, 0, 0); // Unbind the VBO / VAB glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); - + } finally { if (posBuffer != null) memFree(posBuffer); if (indexBuffer != null) memFree(indexBuffer); @@ -78,63 +81,84 @@ public Mesh(float[] posArray, int[] indexArray, float[] coordArray) { public int getVaoId() { return this.vaoId; } public int getVertexCount() { return this.vertexCount; } - + public Texture getTexture() { return this.texture; } public Mesh setTexture(Texture texture) { this.texture = texture; return this; } public boolean isTextured() { return this.texture != null; } - + public Vector4f getColor() { return this.color; } - public Mesh setColor(float r, float g, float b) { this.setColor(new Vector3f(r, g, b)); return this; } - public Mesh setColor(float r, float g, float b, float a) { this.setColor(new Vector4f(r, g, b, a)); return this; } - public Mesh setColor(Vector3f color) { this.setColor(new Vector4f(color, 1f)); return this; } - public Mesh setColor(Vector4f color) { this.color.set(color); return this; } - + public Mesh setColor(Vector3f color) { return this.setColor(color.x, color.y, color.z, 1f); } + public Mesh setColor(Vector4f color) { return this.setColor(color.x, color.y, color.z, color.w); } + public Mesh setColor(float r, float g, float b) { return this.setColor(r, g, b, 1f); } + public Mesh setColor(float r, float g, float b, float a) { this.color.set(r, g, b, a); return this; } + + public void render(Shader shader, Stream items, BiConsumer consumer) { + this.render(shader, (Iterable) items::iterator, consumer); + } + public void render(Shader shader, Iterable items, BiConsumer consumer) { + this.with(shader, () -> { + for (T item : items) { + consumer.accept(shader, item); + this.draw(); + } + }); + } + + public void render(Shader shader, T item, BiConsumer consumer) { + this.with(shader, () -> { + consumer.accept(shader, item); + this.draw(); + }); + } + + protected void with(Shader shader, Runnable runnable) { + this.bind(); + this.setup(shader); + runnable.run(); + this.unbind(); + } + // prepare mesh - public void prepare() { this.prepare(null); } - public void prepare(Mesh lastMesh) { - if (this == lastMesh) - return; + protected void bind() { if (this.isTextured()) - this.texture.prepare(); + this.texture.bind(); glBindVertexArray(this.vaoId); } - + + // setup uniforms + protected void setup(Shader shader) { + shader.set("color", this.color); + shader.set("isTextured", this.isTextured()); + } + // draw elements - public void render() { + protected void draw() { glDrawElements(GL_TRIANGLES, this.vertexCount, GL_UNSIGNED_INT, 0); } - - // Restore state - public void restore() { this.restore(null); } - public void restore(Mesh nextMesh) { - if (this == nextMesh) - return; + + // restore state + protected void unbind() { + if (this.isTextured()) + this.texture.unbind(); glBindVertexArray(0); } - - protected void deleteVbos() { - // Delete the VBO + + @Override + public void close() { this.cleanup(true); } + public void cleanup(boolean closeTexture) { + glDisableVertexAttribArray(0); + + // delete VBO glBindBuffer(GL_ARRAY_BUFFER, 0); for (int id : this.vboIdList) glDeleteBuffers(id); - } - - protected void disableVao() { - glDisableVertexAttribArray(0); - } - - protected void deleteVao() { - // Delete the VAO + + // delete texture + if (closeTexture && this.texture != null) + this.texture.close(); + + // delete VAO glBindVertexArray(0); glDeleteVertexArrays(this.vaoId); } - - public void cleanup() { this.cleanup(true); } - public void cleanup(boolean cleanupTexture) { - this.disableVao(); - this.deleteVbos(); - if (cleanupTexture && this.isTextured()) - this.texture.cleanup(); - this.deleteVao(); - } } diff --git a/src/engine/Mouse.java b/src/engine/Mouse.java index f15ef9f..f608978 100644 --- a/src/engine/Mouse.java +++ b/src/engine/Mouse.java @@ -8,10 +8,10 @@ import org.joml.Vector2f; import static org.lwjgl.glfw.GLFW.*; -public class Mouse { +public class Mouse implements Initializable, Inputtable { private final Vector2f current; private final Vector2f movement; - private Vector2f previous; + private final Vector2f previous; private boolean inside = false; private boolean left = false; @@ -23,32 +23,37 @@ public Mouse() { this.previous = new Vector2f(0, 0); } - public void init(Window window) { - glfwSetCursorPosCallback(window.getHandle(), (handle, x, y) -> { - this.current.x = (float) x; - this.current.y = (float) y; - }); - glfwSetCursorEnterCallback(window.getHandle(), (handle, entered) -> { - this.inside = entered; - }); - glfwSetMouseButtonCallback(window.getHandle(), (handle, button, action, mode) -> { - if (button == GLFW_MOUSE_BUTTON_1) this.left = (action == GLFW_PRESS); - if (button == GLFW_MOUSE_BUTTON_2) this.right = (action == GLFW_PRESS); - }); - } - public Vector2f getMovement() { return this.movement; } public Vector2f getCurrent() { return this.current; } public boolean isInside() { return this.inside; } public boolean isLeft() { return this.left; } public boolean isRight() { return this.right; } + @Override + public void init(Window window) { + glfwSetCursorPosCallback(window.getHandle(), (handle, x, y) -> { this.setCurrent((float) x, (float) y); }); + glfwSetCursorEnterCallback(window.getHandle(), (handle, inside) -> { this.setInside(inside); }); + glfwSetMouseButtonCallback(window.getHandle(), (handle, button, action, mode) -> { + if (button == GLFW_MOUSE_BUTTON_1) this.setLeft(action == GLFW_PRESS); + if (button == GLFW_MOUSE_BUTTON_2) this.setRight(action == GLFW_PRESS); + }); + } + + public void setCurrent(float x, float y) { this.current.set(x, y); } + public void setInside(boolean inside) { this.inside = inside; } + public void setLeft(boolean left) { this.left = left; } + public void setRight(boolean right) { this.right = right; } + + @Override public void input(Window window) { + this.flush(); + } + + public void flush() { this.current.sub(this.previous, this.movement); - this.movement.div(window.getTargetUps() * window.getElapsedTime()); this.previous.set(this.current); } - + public String toString() { return String.format( "<%s movement=%s current=%s>", diff --git a/src/engine/ObjLoader.java b/src/engine/ObjLoader.java index ac80374..7bffb8b 100644 --- a/src/engine/ObjLoader.java +++ b/src/engine/ObjLoader.java @@ -9,17 +9,24 @@ import org.joml.*; public class ObjLoader { - public static Mesh loadMesh(String file) throws Exception { - List vertices = new ArrayList<>(); - List textures = new ArrayList<>(); - List faces = new ArrayList<>(); - + public static Mesh loadMesh(String obj, String texture) { + Mesh mesh = loadMesh(obj); + mesh.setTexture(new Texture(texture)); + return mesh; + } + + public static Mesh loadMesh(String file) { + List posList = new ArrayList<>(); + List indexList = new ArrayList<>(); + List coordList = new ArrayList<>(); + List faceList = new ArrayList<>(); + Utils.loadLinesStream(file).forEach(line -> { String[] tokens = line.split("\\s+"); switch (tokens[0]) { case "v": // Geometric vertex - vertices.add(new Vector3f( + posList.add(new Vector3f( Float.parseFloat(tokens[1]), Float.parseFloat(tokens[2]), Float.parseFloat(tokens[3]) @@ -27,105 +34,119 @@ public static Mesh loadMesh(String file) throws Exception { break; case "vt": // Texture coordinate - textures.add(new Vector2f( + coordList.add(new Vector2f( Float.parseFloat(tokens[1]), Float.parseFloat(tokens[2]) )); break; case "f": Face face = new Face(tokens[1], tokens[2], tokens[3]); - faces.add(face); + faceList.add(face); break; default: // Ignore other lines break; } }); - return reorderLists(vertices, textures, faces); - } - - private static Mesh reorderLists( - List vertexList, - List coordList, - List faceList - ) { - List posList = new ArrayList<>(); + // Create position array in the order it has been declared - float[] posArray = new float[vertexList.size() * 3]; - for (int i = 0; i < vertexList.size(); i++) { - Vector3f pos = vertexList.get(i); + float[] posArray = new float[posList.size() * 3]; + float[] coordArray = new float[posList.size() * 2]; + for (int i = 0; i < posList.size(); i++) { + Vector3f pos = posList.get(i); posArray[i*3 + 0] = pos.x; posArray[i*3 + 1] = pos.y; posArray[i*3 + 2] = pos.z; } - float[] coordArray = new float[vertexList.size() * 2]; - - for (Face face : faceList) - for (IndexGroup group : face.groups) - processFaceVertex(group, coordList, posList, coordArray); - - // int[] indexArray = new int[indices.size()]; - int[] indexArray = Utils.intListToArray(posList); - return new Mesh(posArray, indexArray, coordArray); - } + for (int i = 0; i < coordArray.length; i++) + coordArray[i] = -1; + + for (Face face : faceList) { + for (Group group : face.groups) { + int index = group.index - 1; + + // Set pos for vertex coordinates + indexList.add(index); - private static void processFaceVertex( - IndexGroup group, - List coordList, - List posList, - float[] coordArray - ) { - // Set pos for vertex coordinates - int pos = group.pos; - posList.add(pos); - - // Reorder texture coordinates - if (group.coord != IndexGroup.NO_VALUE) { - Vector2f coord = coordList.get(group.coord); - coordArray[pos*2 + 0] = coord.x; - coordArray[pos*2 + 1] = 1 - coord.y; + // Reorder texture coordinates + if (group.coord != Group.NO_VALUE) { + int coord = group.coord - 1; + Vector2f coordVec = coordList.get(coord); + + float cX = coordVec.x; + float cY = 1 - coordVec.y; + float aX = coordArray[index*2 + 0]; + float aY = coordArray[index*2 + 1]; + + if ((aX != -1 && aX != cX) || (aX != -1 && aY != cY)) + System.err.println( + "ObjLoader texture coord already defined ["+aX+","+aY+"]: " + +file+" f "+group+" ["+cX+","+cY+"]" + ); + coordArray[index*2 + 0] = cX; + coordArray[index*2 + 1] = cY; + } + } } + + int[] indexArray = Utils.intListToArray(indexList); + return new Mesh(posArray, indexArray, coordArray); } - - protected static class IndexGroup { + + protected static class Group { public static final int NO_VALUE = -1; - public int pos; + public int index; public int coord; - - public IndexGroup() { - this.pos = NO_VALUE; + public int normal; + + public Group() { + this.index = NO_VALUE; this.coord = NO_VALUE; + this.normal = NO_VALUE; + } + + public String toString() { + return ( + this.index + +"/"+(this.coord != NO_VALUE ? this.coord : "") + +"/"+(this.normal != NO_VALUE ? this.normal : "") + ); } } - + protected static class Face { // List of pos groups for a face triangle (3 vertices per face). - public final IndexGroup[] groups; + public final Group[] groups; public Face(String v1, String v2, String v3) { - this.groups = new IndexGroup[3]; + this.groups = new Group[3]; // Parse the lines this.groups[0] = parseLine(v1); this.groups[1] = parseLine(v2); this.groups[2] = parseLine(v3); } - private IndexGroup parseLine(String line) { - IndexGroup group = new IndexGroup(); + private static Group parseLine(String line) { + Group group = new Group(); String[] tokens = line.split("/"); int length = tokens.length; - - group.pos = Integer.parseInt(tokens[0]) - 1; + + group.index = Integer.parseInt(tokens[0]); if (length <= 1) return group; - - // It can be empty if the obj does not define text coords + + // can be empty if (tokens[1].length() != 0) - group.coord = Integer.parseInt(tokens[1]) - 1; + group.coord = Integer.parseInt(tokens[1]); if (length <= 2) return group; - + + if (tokens[2].length() != 0) + group.normal = Integer.parseInt(tokens[2]); + if (length <= 3) + return group; + return group; } } diff --git a/src/engine/Renderable.java b/src/engine/Renderable.java new file mode 100644 index 0000000..12f530a --- /dev/null +++ b/src/engine/Renderable.java @@ -0,0 +1,10 @@ +/* +George Zhang +Renderable interface. +*/ + +package geetransit.minecraft05.engine; + +public interface Renderable { + void render(Window window); +} diff --git a/src/engine/Renderer.java b/src/engine/Renderer.java deleted file mode 100644 index 8b396b5..0000000 --- a/src/engine/Renderer.java +++ /dev/null @@ -1,184 +0,0 @@ -/* -ahbejarano -Renderer class. -*/ - -package geetransit.minecraft05.engine; - -import java.util.*; -import org.joml.Matrix4f; -import static org.lwjgl.opengl.GL30.*; - -public abstract class Renderer { - protected Transformation transformation; - - public Renderer() { - this.transformation = new Transformation(); - } - - // create shaders - public abstract void init(Window window) throws Exception; - - // render scene - public abstract void render(Window window); - - // destroy shaders - public abstract void cleanup(); - - public Shader createShader(String vertex, String fragment) throws Exception { - Shader shader = new Shader(); - shader.createVertexShader(Utils.loadResource(vertex)); - shader.createFragmentShader(Utils.loadResource(fragment)); - shader.link(); - return shader; - } - - public Shader create3D(String vertex, String fragment) throws Exception { - Shader shader = this.createShader(vertex, fragment); - shader.createUniform("projectionMatrix"); - shader.createUniform("modelViewMatrix"); - shader.createUniform("texture_sampler"); - shader.createUniform("color"); - shader.createUniform("isTextured"); - shader.createUniform("isSelected"); - return shader; - } - - public Shader create2D(String vertex, String fragment) throws Exception { - Shader shader = this.createShader(vertex, fragment); - shader.createUniform("projModelMatrix"); - shader.createUniform("texture_sampler"); - shader.createUniform("color"); - shader.createUniform("isTextured"); - return shader; - } - - public void render3D(Shader shader, Window window, Camera camera, List items) { - shader.bind(); - - glEnable(GL_CULL_FACE); - glCullFace(GL_BACK); - - // projection - Matrix4f projectionMatrix = this.transformation.getProjectionMatrix(window, camera); - shader.setUniform("projectionMatrix", projectionMatrix); - - // view - Matrix4f viewMatrix = camera.getViewMatrix(); - - // Draw meshes - shader.setUniform("texture_sampler", 0); - int itemsSize = items.size(); - for (int i = 0; i < itemsSize; i++) { - Item item = items.get(i); - Mesh mesh = item.getMesh(); - Matrix4f modelViewMatrix = this.transformation.getModelViewMatrix(item, viewMatrix); - shader.setUniform("modelViewMatrix", modelViewMatrix); - shader.setUniform("color", mesh.getColor()); - shader.setUniform("isTextured", mesh.isTextured()); - shader.setUniform("isSelected", item.isSelected()); - mesh.prepare(this.getMeshFromItems(items, i-1, itemsSize)); - mesh.render(); - mesh.restore(this.getMeshFromItems(items, i+1, itemsSize)); - } - - glDisable(GL_CULL_FACE); - - shader.unbind(); - } - - // note does NOT call Item.render(Window) - public void render3DMap(Shader shader, Window window, Camera camera, Map> map) { - shader.bind(); - glEnable(GL_CULL_FACE); - glCullFace(GL_BACK); - - // projection - Matrix4f projectionMatrix = this.transformation.getProjectionMatrix(window, camera); - shader.setUniform("projectionMatrix", projectionMatrix); - - // view - Matrix4f viewMatrix = camera.getViewMatrix(); - - // Draw meshes - shader.setUniform("texture_sampler", 0); - for (Map.Entry> entry : map.entrySet()) { - Mesh mesh = entry.getKey(); - List items = entry.getValue(); - shader.setUniform("color", mesh.getColor()); - shader.setUniform("isTextured", mesh.isTextured()); - mesh.prepare(); - - // single : loop through items - for (Item item : items) { - Matrix4f modelViewMatrix = this.transformation.getModelViewMatrix(item, viewMatrix); - shader.setUniform("modelViewMatrix", modelViewMatrix); - shader.setUniform("isSelected", item.isSelected()); - mesh.render(); - } - - mesh.restore(); - } - - glDisable(GL_CULL_FACE); - shader.unbind(); - } - - public void render2D(Shader shader, Window window, List items) { - shader.bind(); - - // source # https://stackoverflow.com/a/5467636 - glDepthMask(false); // disable writes to Z-Buffer - glDisable(GL_DEPTH_TEST); // disable depth-testing - - Matrix4f orthoMatrix = this.transformation.getOrthoProjectionMatrix(window); - - // Draw meshes - shader.setUniform("texture_sampler", 0); - for (Item item : items) { - Matrix4f projModelMatrix = this.transformation.getOrthoProjModelMatrix(item, orthoMatrix); - shader.setUniform("projModelMatrix", projModelMatrix); - shader.setUniform("color", item.getMesh().getColor()); - shader.setUniform("isTextured", item.getMesh().isTextured()); - item.render(window); - } - - glDepthMask(true); - glEnable(GL_DEPTH_TEST); - - shader.unbind(); - } - - public void renderSkybox(Shader shader, Window window, Camera camera, List items) { - shader.bind(); - - // projection - Matrix4f projectionMatrix = this.transformation.getProjectionMatrix(window, camera); - shader.setUniform("projectionMatrix", projectionMatrix); - - // view - Matrix4f viewMatrix = camera.getViewMatrix(); - - // remove translation (different from render3D) - viewMatrix.setTranslation(0, 0, 0); - - // Draw meshes - shader.setUniform("texture_sampler", 0); - for (Item item : items) { - Matrix4f modelViewMatrix = this.transformation.getModelViewMatrix(item, viewMatrix); - shader.setUniform("modelViewMatrix", modelViewMatrix); - shader.setUniform("color", item.getMesh().getColor()); - shader.setUniform("isTextured", item.getMesh().isTextured()); - item.render(window); - } - - shader.unbind(); - } - - public Mesh getMeshFromItems(List items, int index) { return this.getMeshFromItems(items, index, items.size()); } - public Mesh getMeshFromItems(List items, int index, int size) { - if (0 <= index && index < size) - return items.get(index).getMesh(); - return null; - } -} diff --git a/src/engine/Scene.java b/src/engine/Scene.java index 467d9f3..19fd3ee 100644 --- a/src/engine/Scene.java +++ b/src/engine/Scene.java @@ -1,14 +1,94 @@ /* George Zhang -Scene interface. (used by Window) +Scene class. +Encapsulates a whole scene (init, loop, cleanup) */ package geetransit.minecraft05.engine; -public interface Scene { - void init(Window window) throws Exception; - void input(Window window); - void update(float interval); - void render(Window window); - void cleanup(); +import java.util.*; + +public class Scene implements Loopable { + // bit flags to add to different lists + public static final int INIT = 0b0001; + public static final int INPUT = 0b0010; + public static final int UPDATE = 0b0100; + public static final int RENDER = 0b1000; + public static final int ALL = INIT & INPUT & UPDATE & RENDER; + + private final List inits; + private final List inputs; + private final List updates; + private final List renders; + + public Scene() { + this.inits = new ArrayList<>(); + this.inputs = new ArrayList<>(); + this.updates = new ArrayList<>(); + this.renders = new ArrayList<>(); + } + + public List getInits() { return this.inits; } + public List getInputs() { return this.inputs; } + public List getUpdates() { return this.updates; } + public List getRenders() { return this.renders; } + + public Scene addInit(Initializable init) { this.inits.add(init); return this; } + public Scene addInput(Inputtable input) { this.inputs.add(input); return this; } + public Scene addUpdate(Updateable update) { this.updates.add(update); return this; } + public Scene addRender(Renderable render) { this.renders.add(render); return this; } + + public Scene addTo(int flags, Object obj) { + if ((flags & INIT) != 0) + this.addInit((Initializable) obj); + if ((flags & INPUT) != 0) + this.addInput((Inputtable) obj); + if ((flags & UPDATE) != 0) + this.addUpdate((Updateable) obj); + if ((flags & RENDER) != 0) + this.addRender((Renderable) obj); + return this; + } + + public Scene addFrom(Object obj) { + if (obj instanceof Initializable) + this.addTo(INIT, obj); + if (obj instanceof Inputtable) + this.addTo(INPUT, obj); + if (obj instanceof Updateable) + this.addTo(UPDATE, obj); + if (obj instanceof Renderable) + this.addTo(RENDER, obj); + return this; + } + + @Override + public void init(Window window) { + for (Initializable init : this.getInits()) + init.init(window); + } + + @Override + public void input(Window window) { + for (Inputtable input : this.getInputs()) + input.input(window); + } + + @Override + public void render(Window window) { + for (Renderable render : this.getRenders()) + render.render(window); + } + + @Override + public void update(float interval) { + for (Updateable update : this.getUpdates()) + update.update(interval); + } + + @Override + public void cleanup() { + for (Initializable init : this.getInits()) + init.cleanup(); + } } diff --git a/src/engine/SceneBase.java b/src/engine/SceneBase.java deleted file mode 100644 index d9340a4..0000000 --- a/src/engine/SceneBase.java +++ /dev/null @@ -1,44 +0,0 @@ -/* -George Zhang -Base scene implementation. (no rendering) -*/ - -package geetransit.minecraft05.engine; - -import java.util.*; - -public abstract class SceneBase implements Scene { - private List scenes; - - public SceneBase() { - this.scenes = new ArrayList<>(); - } - - public List getScenes() { return this.scenes; } - public SceneBase addScene(Scene scene) { this.scenes.add(scene); return this; } - - public void init(Window window) throws Exception { - for (Scene scene : this.getScenes()) - scene.init(window); - } - - public void input(Window window) { - for (Scene scene : this.getScenes()) - scene.input(window); - } - - public void update(float interval) { - for (Scene scene : this.getScenes()) - scene.update(interval); - } - - public void render(Window window) { - for (Scene scene : this.getScenes()) - scene.render(window); - } - - public void cleanup() { - for (Scene scene : this.getScenes()) - scene.cleanup(); - } -} diff --git a/src/engine/SceneRender.java b/src/engine/SceneRender.java deleted file mode 100644 index 1e9c202..0000000 --- a/src/engine/SceneRender.java +++ /dev/null @@ -1,70 +0,0 @@ -/* -George Zhang -Base rendered scene implementation. -*/ - -package geetransit.minecraft05.engine; - -import java.util.*; - -public abstract class SceneRender extends SceneBase { - private Renderer renderer; - private List items; - private Map> meshMap; - - public SceneRender(Renderer renderer) { - super(); - this.renderer = renderer; - this.items = new ArrayList<>(); - this.meshMap = new HashMap<>(); - } - public SceneRender() { - this(null); - } - - public Renderer getRenderer() { return this.renderer; } - public SceneRender setRenderer(Renderer renderer) { this.renderer = renderer; return this; } - - public List getItems() { return this.items; } - public Map> getMeshMap() { return this.meshMap; } - public SceneRender addItem(Item item) { - Mesh mesh = item.getMesh(); - this.items.add(item); - this.meshMap.putIfAbsent(mesh, new ArrayList<>()); - this.meshMap.get(mesh).add(item); - return this; - } - public SceneRender removeItem(Item item) { - Mesh mesh = item.getMesh(); - this.items.remove(item); - this.meshMap.get(mesh).remove(item); - if (this.meshMap.get(mesh).size() == 0) - this.meshMap.remove(mesh); - return this; - } - - @Override - public void init(Window window) throws Exception { - this.renderer.init(window); - super.init(window); - } - - @Override - public void input(Window window) { - super.input(window); - } - - @Override - public void render(Window window) { - this.renderer.render(window); - super.render(window); - } - - @Override - public void cleanup() { - this.renderer.cleanup(); - super.cleanup(); - for (Item item : this.getItems()) - item.cleanup(); - } -} diff --git a/src/engine/Shader.java b/src/engine/Shader.java index b74936b..0f0d9d5 100644 --- a/src/engine/Shader.java +++ b/src/engine/Shader.java @@ -8,112 +8,103 @@ import java.util.*; import org.joml.*; import org.lwjgl.system.*; -import static org.lwjgl.opengl.GL20.*; -public class Shader { - private final int programId; +import static org.lwjgl.opengl.GL20.*; - private int vertexShaderId; - private int fragmentShaderId; - +public class Shader implements AutoCloseable { + private final int program; private final Map uniforms; - public Shader() throws Exception { - this.programId = glCreateProgram(); - if (this.programId == 0) { - throw new Exception("Could not create Shader"); - } + private int vertex; + private int fragment; + + public Shader() { + this.program = glCreateProgram(); + if (this.program == 0) + throw new RuntimeException("Could not create Shader"); this.uniforms = new HashMap<>(); } - - public void createUniform(String name) throws Exception { - int location = glGetUniformLocation(this.programId, name); - if (location < 0) { - throw new Exception("Could not find uniform:" + name); - } - uniforms.put(name, location); + + public void create(String name) { + int location = glGetUniformLocation(this.program, name); + if (location < 0) + throw new RuntimeException("Could not find uniform:" + name); + this.uniforms.put(name, location); } - - public void setUniform(String name, Matrix4f value) { + + public void set(String name, Matrix4f value) { // Dump the matrix into a float buffer try (MemoryStack stack = MemoryStack.stackPush()) { - glUniformMatrix4fv(uniforms.get(name), false, value.get(stack.mallocFloat(16))); + glUniformMatrix4fv(this.uniforms.get(name), false, value.get(stack.mallocFloat(16))); } } - public void setUniform(String name, int value) { - glUniform1i(uniforms.get(name), value); + public void set(String name, int value) { + glUniform1i(this.uniforms.get(name), value); } - public void setUniform(String name, boolean value) { - glUniform1i(uniforms.get(name), value ? 1 : 0); + public void set(String name, boolean value) { + this.set(name, value ? 1 : 0); } - public void setUniform(String name, Vector3f value) { - glUniform3f(uniforms.get(name), value.x, value.y, value.z); + public void set(String name, Vector3f value) { + glUniform3f(this.uniforms.get(name), value.x, value.y, value.z); } - public void setUniform(String name, Vector4f value) { - glUniform4f(uniforms.get(name), value.x, value.y, value.z, value.w); + public void set(String name, Vector4f value) { + glUniform4f(this.uniforms.get(name), value.x, value.y, value.z, value.w); } - public void createVertexShader(String shaderCode) throws Exception { - this.vertexShaderId = createShader(shaderCode, GL_VERTEX_SHADER); + public void compileVertex(String code) { + this.vertex = this.compile(code, GL_VERTEX_SHADER); } - public void createFragmentShader(String shaderCode) throws Exception { - this.fragmentShaderId = createShader(shaderCode, GL_FRAGMENT_SHADER); + public void compileFragment(String code) { + this.fragment = this.compile(code, GL_FRAGMENT_SHADER); } - protected int createShader(String shaderCode, int shaderType) throws Exception { - int shaderId = glCreateShader(shaderType); - if (shaderId == 0) { - throw new Exception("Error creating shader. Type: " + shaderType); - } + protected int compile(String code, int type) { + int id = glCreateShader(type); + if (id == 0) + throw new RuntimeException("Error creating shader. Type: " + type); - glShaderSource(shaderId, shaderCode); - glCompileShader(shaderId); + glShaderSource(id, code); + glCompileShader(id); - if (glGetShaderi(shaderId, GL_COMPILE_STATUS) == 0) { - throw new Exception("Error compiling Shader code: " + glGetShaderInfoLog(shaderId, 1024)); - } - glAttachShader(programId, shaderId); + if (glGetShaderi(id, GL_COMPILE_STATUS) == 0) + throw new RuntimeException("Error compiling Shader code: " + glGetShaderInfoLog(id, 1024)); + glAttachShader(this.program, id); - return shaderId; + return id; } - public void link() throws Exception { - glLinkProgram(this.programId); - if (glGetProgrami(this.programId, GL_LINK_STATUS) == 0) { - throw new Exception("Error linking Shader code: " + glGetProgramInfoLog(this.programId, 1024)); - } + public void link() { + glLinkProgram(this.program); + if (glGetProgrami(this.program, GL_LINK_STATUS) == 0) + throw new RuntimeException("Error linking Shader code: " + glGetProgramInfoLog(this.program, 1024)); + + if (this.vertex != 0) + glDetachShader(this.program, this.vertex); + if (this.fragment != 0) + glDetachShader(this.program, this.fragment); - if (this.vertexShaderId != 0) { - glDetachShader(this.programId, this.vertexShaderId); - } - if (this.fragmentShaderId != 0) { - glDetachShader(this.programId, this.fragmentShaderId); - } - // definition in res/vertex.vs // equivalent of `layout (location = #) ...` - glBindAttribLocation(this.programId, 0, "position"); - glBindAttribLocation(this.programId, 1, "coords"); - glValidateProgram(this.programId); - if (glGetProgrami(this.programId, GL_VALIDATE_STATUS) == 0) { - System.err.println("Warning validating Shader code: " + glGetProgramInfoLog(this.programId, 1024)); - } - + glBindAttribLocation(this.program, 0, "position"); + glBindAttribLocation(this.program, 1, "coords"); + glValidateProgram(this.program); + if (glGetProgrami(this.program, GL_VALIDATE_STATUS) == 0) + System.err.println("Warning validating Shader code: " + glGetProgramInfoLog(this.program, 1024)); } public void bind() { - glUseProgram(this.programId); + glUseProgram(this.program); } public void unbind() { glUseProgram(0); } - public void cleanup() { + @Override + public void close() { this.unbind(); - if (this.programId != 0) { - glDeleteProgram(this.programId); - } + if (this.program != 0) + glDeleteProgram(this.program); } -} \ No newline at end of file +} diff --git a/src/engine/TextItem.java b/src/engine/TextItem.java index 7dca5c6..b9e4614 100644 --- a/src/engine/TextItem.java +++ b/src/engine/TextItem.java @@ -1,112 +1,40 @@ /* ahbejarano -Game item wrapper class. +Text item class. */ package geetransit.minecraft05.engine; -import java.util.*; -import java.nio.charset.Charset; +import org.joml.Vector3f; import org.joml.Vector4f; +import org.joml.Quaternionf; public class TextItem extends Item { - private static final float ZPOS = 0f; - private static final int VERTICES_PER_QUAD = 4; - private String text; - private final int fontCols; - private final int fontRows; - - public TextItem(String text, String fontFile, int fontCols, int fontRows) throws Exception { - super(); + private final FontTexture fontTexture; + + public TextItem(String text, FontTexture fontTexture) { + super(fontTexture.buildMesh(text)); this.text = text; - this.fontCols = fontCols; - this.fontRows = fontRows; - this.mesh = this.buildMesh(new Texture(fontFile)); - + this.fontTexture = fontTexture; } - + + protected TextItem setMesh(Mesh mesh) { super.setMesh(mesh); return this; } + public TextItem setPosition(Vector3f position) { super.setPosition(position); return this; } + public TextItem setPosition(float x, float y, float z) { super.setPosition(x, y, z); return this; } + public TextItem setRotation(Quaternionf rotation) { super.setRotation(rotation); return this; } + public TextItem setRotation(float x, float y, float z) { super.setRotation(x, y, z); return this; } + public TextItem setScale(float scale) { super.setScale(scale); return this; } + public String getText() { return this.text; } - public int getFontCols() { return this.fontCols; } - public int getFontRows() { return this.fontRows; } - - public Item setText(String text) { + public FontTexture getFontTexture() { return this.fontTexture; } + + public TextItem setText(String text) { this.text = text; - Vector4f color = this.mesh.getColor(); - this.mesh.cleanup(false); - this.mesh = this.buildMesh(this.mesh.getTexture()); - this.mesh.setColor(color); + Vector4f color = this.getMesh().getColor(); + this.getMesh().cleanup(false); + this.setMesh(this.fontTexture.buildMesh(this.text)); + this.getMesh().setColor(color); return this; } - - private Mesh buildMesh(Texture texture) { - byte[] charArray = this.text.getBytes(Charset.forName("ISO-8859-1")); - - List posList = new ArrayList<>(); - List coordList = new ArrayList<>(); - List indexList = new ArrayList<>(); - - float width = (float) texture.getWidth() / this.fontCols; - float length = (float) texture.getLength() / this.fontRows; - - int currentCol = 0; - int currentRow = 0; - int currentIndex = 0; - for (int i = 0; i < charArray.length; i++) { - byte currentChar = charArray[i]; - if (currentChar == '\n') { - currentRow++; - currentCol = 0; - continue; - } - - // Build a character tile composed by two triangles - int fontCol = currentChar % this.fontCols; - int fontRow = currentChar / this.fontCols; - - // Left Top vertex - posList.add(currentCol*width); // x - posList.add(currentRow*length); // y - posList.add(ZPOS); // z - coordList.add((float) fontCol / this.fontCols); - coordList.add((float) fontRow / this.fontRows); - indexList.add(currentIndex*VERTICES_PER_QUAD + 0); - - // Left Bottom vertex - posList.add(currentCol*width); // x - posList.add(currentRow*length + length); // y - posList.add(ZPOS); // z - coordList.add((float) fontCol / this.fontCols); - coordList.add((float) (fontRow + 1) / this.fontRows); - indexList.add(currentIndex*VERTICES_PER_QUAD + 1); - - // Right Bottom vertex - posList.add(currentCol*width + width); // x - posList.add(currentRow*length + length); // y - posList.add(ZPOS); // z - coordList.add((float) (fontCol + 1) / this.fontCols); - coordList.add((float) (fontRow + 1) / this.fontRows); - indexList.add(currentIndex*VERTICES_PER_QUAD + 2); - - // Right Top vertex - posList.add(currentCol*width + width); // x - posList.add(currentRow*length); // y - posList.add(ZPOS); // z - coordList.add((float) (fontCol + 1) / this.fontCols); - coordList.add((float) fontRow / this.fontRows); - indexList.add(currentIndex*VERTICES_PER_QUAD + 3); - - // Add indices for left top and bottom right vertices - indexList.add(currentIndex*VERTICES_PER_QUAD + 0); - indexList.add(currentIndex*VERTICES_PER_QUAD + 2); - - currentCol++; - currentIndex++; - } - - float[] posArray = Utils.floatListToArray(posList); - float[] coordArray = Utils.floatListToArray(coordList); - int[] indexArray = Utils.intListToArray(indexList); - return new Mesh(posArray, indexArray, coordArray).setTexture(texture); - } } diff --git a/src/engine/Texture.java b/src/engine/Texture.java index eccb722..0e966a3 100644 --- a/src/engine/Texture.java +++ b/src/engine/Texture.java @@ -8,60 +8,57 @@ import java.nio.*; import static org.lwjgl.opengl.GL30.*; -import static org.lwjgl.system.MemoryUtil.*; -public class Texture { +public class Texture implements AutoCloseable { + private final int id; + private final int width; + private final int length; - private int id; - private int width; - private int length; + public Texture(String file) { + try (Image image = new Image(file)) { + this.width = image.width; + this.length = image.length; - public Texture(String fileName) throws Exception { - this.loadTexture(fileName); + // Create a new OpenGL texture + this.id = glGenTextures(); + // Bind the texture + glBindTexture(GL_TEXTURE_2D, this.id); + + // Tell OpenGL how to unpack the RGBA bytes. Each component is 1 byte size + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + + // make text easier to read + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + + // Upload the texture data + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, this.width, this.length, 0, GL_RGBA, GL_UNSIGNED_BYTE, image.buffer); + // Generate Mip Map + glGenerateMipmap(GL_TEXTURE_2D); + } } - public Texture(int id, int width, int length) throws Exception { + + protected Texture(int id, int width, int length) { this.id = id; this.width = width; this.length = length; } - + public int getId() { return this.id; } public int getWidth() { return this.width; } public int getLength() { return this.length; } public void bind() { - glBindTexture(GL_TEXTURE_2D, this.id); - } - public void prepare() { glActiveTexture(GL_TEXTURE0); - this.bind(); - } - public void cleanup() { - glDeleteTextures(this.id); + glBindTexture(GL_TEXTURE_2D, this.id); } - - private void loadTexture(String fileName) throws Exception { - ByteBuffer image = Utils.loadImage(fileName, (w, l) -> { this.width = w; this.length = l; }); - - // Create a new OpenGL texture - int textureId = glGenTextures(); - // Bind the texture - glBindTexture(GL_TEXTURE_2D, textureId); - // Tell OpenGL how to unpack the RGBA bytes. Each component is 1 byte size - glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - - // make text easier to read - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + public void unbind() { + glBindTexture(GL_TEXTURE_2D, 0); + } - // Upload the texture data - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, this.width, this.length, 0, GL_RGBA, GL_UNSIGNED_BYTE, image); - // Generate Mip Map - glGenerateMipmap(GL_TEXTURE_2D); - - Utils.freeImage(image); - - this.id = textureId; + @Override + public void close() { + glDeleteTextures(this.id); } } diff --git a/src/engine/Ticker.java b/src/engine/Ticker.java new file mode 100644 index 0000000..1c656cd --- /dev/null +++ b/src/engine/Ticker.java @@ -0,0 +1,46 @@ +/* +George Zhang +Ticker class. +*/ + +package geetransit.minecraft05.engine; + +public class Ticker { + private float interval; + private float accumulated; + + public Ticker(float interval) { + this.interval = interval; + this.accumulated = 0f; + } + + public float getAccumulated() { return this.accumulated; } + public float getInterval() { return this.interval; } + public Ticker setInterval(float interval) { this.interval = interval; return this; } + + public Ticker add(float time) { + this.accumulated += time; + return this; + } + + public Ticker reset() { + this.accumulated = 0f; + return this; + } + + // while (ticker.next()) + public boolean next() { + if (this.accumulated < this.interval) + return false; + this.accumulated -= this.interval; + return true; + } + + // if (ticker.nextOnce()) + public boolean nextOnce() { + if (this.accumulated < this.interval) + return false; + this.accumulated %= this.interval; + return true; + } +} diff --git a/src/engine/Timer.java b/src/engine/Timer.java index 8fdaebb..0b3fc5c 100644 --- a/src/engine/Timer.java +++ b/src/engine/Timer.java @@ -8,24 +8,24 @@ public class Timer { - private double lastLoopTime; - - public void init() { - this.lastLoopTime = getTime(); - } + private double lastLoopTime; - public double getTime() { - return System.nanoTime() / 1000_000_000.0; - } + public void init() { + this.lastLoopTime = getTime(); + } - public float getElapsedTime() { - double time = getTime(); - float elapsedTime = (float) (time - this.lastLoopTime); - this.lastLoopTime = time; - return elapsedTime; - } + public double getTime() { + return System.nanoTime() / 1000_000_000.0; + } - public double getLastLoopTime() { - return this.lastLoopTime; - } + public float getElapsedTime() { + double time = getTime(); + float elapsedTime = (float) (time - this.lastLoopTime); + this.lastLoopTime = time; + return elapsedTime; + } + + public double getLastLoopTime() { + return this.lastLoopTime; + } } diff --git a/src/engine/Transformation.java b/src/engine/Transformation.java deleted file mode 100644 index abf56fb..0000000 --- a/src/engine/Transformation.java +++ /dev/null @@ -1,59 +0,0 @@ -/* -ahbejarano -Transformation helper class. -*/ - -package geetransit.minecraft05.engine; - -import org.joml.Matrix4f; -import org.joml.Vector3f; -import org.joml.Quaternionf; - -public class Transformation { - private final Matrix4f projectionMatrix; - private final Matrix4f modelViewMatrix; - private final Matrix4f orthoProjectionMatrix; - private final Matrix4f orthoProjModelMatrix; - private final Matrix4f modelMatrix; - - public Transformation() { - this.projectionMatrix = new Matrix4f(); - this.modelViewMatrix = new Matrix4f(); - this.orthoProjectionMatrix = new Matrix4f(); - this.orthoProjModelMatrix = new Matrix4f(); - this.modelMatrix = new Matrix4f(); - } - - public Matrix4f getProjectionMatrix(Window window, Camera camera) { - return this.projectionMatrix.setPerspective( - -camera.getFov(), (float) window.getWidth() / window.getHeight(), - camera.getNear(), camera.getFar() - ); - } - - public Matrix4f getModelMatrix(Item item) { - return this.modelMatrix.translationRotateScale(item.getPosition(), item.getRotation(), item.getScale()); - } - - public Matrix4f getModelViewMatrix(Item item, Matrix4f viewMatrix) { - return viewMatrix.mulAffine(this.getModelMatrix(item), this.modelViewMatrix); - } - - public Matrix4f getOrthoProjectionMatrix(Window window) { - return this.orthoProjectionMatrix.setOrtho2D(0, window.getWidth(), window.getHeight(), 0); - } - - // these 2 are different? - public Matrix4f newGetOrthoProjModelMatrix(Item item, Matrix4f orthoMatrix) { - return orthoMatrix.mulOrthoAffine(this.getModelMatrix(item), this.orthoProjModelMatrix); - } - public Matrix4f getOrthoProjModelMatrix(Item item, Matrix4f orthoMatrix) { - return this.orthoProjModelMatrix - .set(orthoMatrix) - .translate(item.getPosition()) - .rotateX((float) Math.toRadians(-item.getRotation().x)) - .rotateY((float) Math.toRadians(-item.getRotation().y)) - .rotateZ((float) Math.toRadians(-item.getRotation().z)) - .scale(item.getScale()); - } -} diff --git a/src/engine/Updateable.java b/src/engine/Updateable.java new file mode 100644 index 0000000..c696c8c --- /dev/null +++ b/src/engine/Updateable.java @@ -0,0 +1,10 @@ +/* +George Zhang +Updateable interface +*/ + +package geetransit.minecraft05.engine; + +public interface Updateable { + void update(float interval); +} diff --git a/src/engine/Utils.java b/src/engine/Utils.java index cbea121..f6bfc60 100644 --- a/src/engine/Utils.java +++ b/src/engine/Utils.java @@ -8,25 +8,23 @@ import java.io.*; import java.util.*; import java.util.stream.*; -import java.util.function.*; import java.nio.*; import java.nio.charset.StandardCharsets; import org.lwjgl.system.*; -import org.lwjgl.stb.*; public class Utils { - - public static InputStream loadInputStream(String file) throws Exception { + + public static InputStream loadInputStream(String file) { InputStream in = Utils.class.getResourceAsStream(file); if (in == null) - throw new Exception("file [" + file + "] does not exist"); + throw new RuntimeException("file [" + file + "] does not exist"); return in; } - + // source # https://stackoverflow.com/a/17861016 - public static byte[] loadByteArray(String file) throws Exception { + public static byte[] loadByteArray(String file) { try ( InputStream in = loadInputStream(file); ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -36,58 +34,39 @@ public static byte[] loadByteArray(String file) throws Exception { while ((len = in.read(buffer)) != -1) out.write(buffer, 0, len); return out.toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e); } } - public static String loadResource(String file) throws Exception { + public static String loadResource(String file) { try ( InputStream in = loadInputStream(file); Scanner scanner = new Scanner(in, StandardCharsets.UTF_8.name()); ) { return scanner.useDelimiter("\\A").next(); + } catch (IOException e) { + throw new RuntimeException(e); } } - public static Stream loadLinesStream(String file) throws Exception { - // source # https://stackoverflow.com/a/30336423 - InputStream in = loadInputStream(file); - InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8.name()); - return new BufferedReader(isr).lines(); - } - - public static ByteBuffer loadImage(String fileName, BiConsumer consumer) throws Exception { - ByteBuffer imageBuffer; - ByteBuffer rawBuffer; - - // Load Texture file - try (MemoryStack stack = MemoryStack.stackPush()) { - IntBuffer widthBuffer = stack.mallocInt(1); - IntBuffer heightBuffer = stack.mallocInt(1); - IntBuffer channelsBuffer = stack.mallocInt(1); - - byte[] array = loadByteArray(fileName); - rawBuffer = MemoryUtil.memAlloc(array.length); - rawBuffer.put(array).flip(); - - imageBuffer = STBImage.stbi_load_from_memory(rawBuffer, widthBuffer, heightBuffer, channelsBuffer, 4); - if (imageBuffer == null) - throw new Exception("Image file [" + fileName + "] not loaded: " + STBImage.stbi_failure_reason()); - - // Get width and height of image - consumer.accept(widthBuffer.get(), heightBuffer.get()); + public static Stream loadLinesStream(String file) { + try { + // source # https://stackoverflow.com/a/30336423 + InputStream in = loadInputStream(file); + InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8.name()); + return new BufferedReader(isr).lines(); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); } - - return imageBuffer; - } - public static ByteBuffer loadImage(String fileName, int[] widthArray, int[] heightArray) throws Exception { - return loadImage(fileName, (width, height) -> { widthArray[0] = width; heightArray[0] = height; }); - } - public static void freeImage(ByteBuffer imageBuffer) { - STBImage.stbi_image_free(imageBuffer); } - + public static int[] intListToArray(List intList) { - return intList.stream().mapToInt(i -> i).toArray(); + int[] intArray = new int[intList.size()]; + int i = 0; + for (int j : intList) + intArray[i++] = j; + return intArray; } public static float[] floatListToArray(List floatList) { float[] floatArray = new float[floatList.size()]; diff --git a/src/engine/Window.java b/src/engine/Window.java index aeed08b..a20f6f0 100644 --- a/src/engine/Window.java +++ b/src/engine/Window.java @@ -7,6 +7,7 @@ import org.lwjgl.glfw.*; import org.lwjgl.opengl.*; +import org.joml.Matrix4f; import static org.lwjgl.glfw.Callbacks.*; import static org.lwjgl.glfw.GLFW.*; @@ -18,53 +19,54 @@ public class Window { private Timer timer; private Object lock; public final Bucket next; - + private String title; private int xPos; private int yPos; private int width; private int height; private int mode; - + public static final int WINDOWED = 0; public static final int BORDERLESS = 1; public static final int FULLSCREEN = 2; - + private boolean vSync; private int targetFps; - private int targetUps; - private float elapsedTime; - private float accumulatedTime; - + + private final Matrix4f projectionMatrix; + private final Matrix4f orthoProjectionMatrix; + private boolean destroyed = false; - + private long monitor; private GLFWVidMode vidmode; - + public Window( String title, int width, int height, int mode, boolean vSync, - int targetFps, - int targetUps + int targetFps ) { this.title = title; this.width = width; this.height = height; this.mode = mode; - + this.vSync = vSync; this.targetFps = targetFps; - this.targetUps = targetUps; - + this.timer = new Timer(); this.lock = new Object(); this.next = new Bucket(); + + this.projectionMatrix = new Matrix4f(); + this.orthoProjectionMatrix = new Matrix4f(); } - + public void createWindow() { // Setup an error callback. The default implementation // will print the error message in System.err. @@ -75,45 +77,45 @@ public void createWindow() { throw new IllegalStateException("Unable to initialize GLFW"); // Configure our window - glfwDefaultWindowHints(); // optional, the current window hints are already the default - glfwWindowHint(GLFW_VISIBLE, GL_FALSE); // the window will stay hidden after creation - glfwWindowHint(GLFW_RESIZABLE, GL_TRUE); // the window will be resizable - glfwWindowHint(GLFW_FOCUSED, GL_TRUE); // get focus when shown + glfwDefaultWindowHints(); // optional, the current window hints are already the default + glfwWindowHint(GLFW_VISIBLE, GL_FALSE); // the window will stay hidden after creation + glfwWindowHint(GLFW_RESIZABLE, GL_TRUE); // the window will be resizable + glfwWindowHint(GLFW_FOCUSED, GL_TRUE); // get focus when shown // Get the resolution of the primary monitor this.monitor = glfwGetPrimaryMonitor(); this.vidmode = glfwGetVideoMode(monitor); - + // Create the window this.handle = glfwCreateWindow(this.width, this.height, this.title, NULL, NULL); if (this.handle == NULL) throw new RuntimeException("Failed to create the GLFW window"); - // Setup resize callback - glfwSetFramebufferSizeCallback(this.handle, (window, width, height) -> { + // Setup resize callback + glfwSetFramebufferSizeCallback(this.handle, (window, width, height) -> { if (width > 0 && height > 0) this.next.add("size", () -> { if (this.isWindowed()) this.setSize(width, height); }); - }); + }); // Center our window this.xPos = this.getCenteredXPos(); this.yPos = this.getCenteredYPos(); this.updateWindowMonitor(); } - + public void eventThread() { this.eventLoop(); this.eventDestroy(); this.eventTerminate(); } - + private void eventLoop() { // Make the window visible glfwShowWindow(this.handle); - + while (!glfwWindowShouldClose(this.handle)) { // Only force focus when in borderless fullscreen if (this.isBorderless()) @@ -123,7 +125,7 @@ private void eventLoop() { glfwWaitEvents(); } } - + private void eventDestroy() { synchronized (this.lock) { this.destroyed = true; @@ -133,52 +135,51 @@ private void eventDestroy() { // Release window callbacks glfwFreeCallbacks(this.handle); } - + public void eventTerminate() { // Terminate GLFW and release the error function glfwTerminate(); glfwSetErrorCallback(null).free(); } - - public void renderThread(Scene scene) { + + public void renderThread(Loopable loop) { try { - this.renderInit(scene); - this.renderLoop(scene); + this.renderInit(loop); + this.renderLoop(loop); } catch (Exception e) { e.printStackTrace(); System.exit(1); } finally { - scene.cleanup(); + loop.cleanup(); } } - - private void renderInit(Scene scene) throws Exception { + + private void renderInit(Loopable loop) throws Exception { // This adds the OpenGL context into this function. glfwMakeContextCurrent(this.handle); - + // This line is critical for LWJGL's interoperation with GLFW's // OpenGL context, or any context that is managed externally. // LWJGL detects the context that is current in the current thread, // creates the ContextCapabilities instance and makes the OpenGL // bindings available for use. GL.createCapabilities(); - + // Check vSync glfwSwapInterval(this.isVSync() ? 1 : 0); - + // init - scene.init(this); - + loop.init(this); + // Start timer. this.timer.init(); } - + // Render loop. - private void renderLoop(Scene scene) { + private void renderLoop(Loopable loop) { while (!this.isDestroyed()) { this.elapsedTime = this.timer.getElapsedTime(); - this.accumulatedTime += this.elapsedTime; - + // bucket this.next.run("mode"); this.next.run("size"); @@ -186,26 +187,21 @@ private void renderLoop(Scene scene) { this.next.run("inputMode"); this.next.run("attrib"); this.next.run("targetFps"); - this.next.run("targetUps"); - + // input - scene.input(this); - + loop.input(this); + // update - float interval = 1f / this.getTargetUps(); - while (this.accumulatedTime >= interval) { - scene.update(interval); - this.accumulatedTime -= interval; - } - + loop.update(this.elapsedTime); + // render - scene.render(this); + loop.render(this); this.renderUpdate(); if (!this.isVSync()) this.renderSync(); } } - + // Sync with target FPS. private void renderSync() { float loopSlot = 1f / this.getTargetFps(); @@ -217,23 +213,36 @@ private void renderSync() { } } } - + private void renderUpdate() { // This can fail if not sync'd. (Can only swap when window exists) synchronized (this.lock) { if (!this.destroyed) - glfwSwapBuffers(this.handle); // swap the color buffers + glfwSwapBuffers(this.handle); // swap the color buffers } } - + + public Matrix4f getProjectionMatrix() { return this.projectionMatrix; } + public Matrix4f buildProjectionMatrix(Camera camera) { + return this.projectionMatrix.setPerspective( + -camera.getFov(), (float) this.getWidth() / this.getHeight(), + camera.getNear(), camera.getFar() + ); + } + + public Matrix4f getOrthoProjectionMatrix() { return this.orthoProjectionMatrix; } + public Matrix4f buildOrthoProjectionMatrix() { + return this.orthoProjectionMatrix.setOrtho2D(0, this.getWidth(), this.getHeight(), 0); + } + protected void updateWindowPos() { - int[] xPos = new int[1]; - int[] yPos = new int[1]; + int[] xPos = {0}; + int[] yPos = {0}; glfwGetWindowPos(this.getHandle(), xPos, yPos); this.xPos = xPos[0]; this.yPos = yPos[0]; } - + protected void updateWindowMonitor() { this.setAttrib(GLFW_DECORATED, this.isBorderless() ? 0 : 1); glfwSetWindowMonitor( @@ -241,49 +250,46 @@ protected void updateWindowMonitor() { this.getWidth(), this.getHeight(), GLFW_DONT_CARE ); } - + protected void updateSwapInterval(boolean vSync) { glfwSwapInterval(vSync ? 1 : 0); } protected void updateSwapInterval() { this.updateSwapInterval(this.isVSync()); } protected long getCurrentMonitor() { return this.isFullscreen() ? this.monitor : NULL; } - + public void setShouldClose(boolean shouldClose) { glfwSetWindowShouldClose(this.getHandle(), true); } public void clear(int bits) { glClear(bits); } public void clearColor(float r, float g, float b, float a) { glClearColor(r, g, b, a); } - + public GLFWKeyCallback setKeyCallback(GLFWKeyCallbackI keyCallback) { return glfwSetKeyCallback(this.getHandle(), keyCallback); } - public void setAttrib(int attrib, int value) { glfwSetWindowAttrib(this.getHandle(), attrib, value); } - public long getAttrib(int attrib) { return glfwGetWindowAttrib(this.getHandle(), attrib); } + public void setAttrib(int attrib, int value) { glfwSetWindowAttrib(this.getHandle(), attrib, value); } + public long getAttrib(int attrib) { return glfwGetWindowAttrib(this.getHandle(), attrib); } public void setInputMode(int mode, int value) { glfwSetInputMode(this.getHandle(), mode, value); } - public long getInputMode(int mode) { return glfwGetInputMode(this.getHandle(), mode); } - public int getKey(int key) { return glfwGetKey(this.getHandle(), key); } - public boolean isKeyDown(int key) { return (this.getKey(key) == GLFW_PRESS); } - public void postEmptyEvent() { glfwPostEmptyEvent(); } - - public long getHandle() { return this.handle; } - public Object getLock() { return this.lock; } - public boolean isDestroyed() { return this.destroyed; } - public float getElapsedTime() { return this.elapsedTime; } - public float getAccumulatedTime() { return this.accumulatedTime; } - - public int getTargetFps() { return this.targetFps; } - public int getTargetUps() { return this.targetUps; } + public long getInputMode(int mode) { return glfwGetInputMode(this.getHandle(), mode); } + public int getKey(int key) { return glfwGetKey(this.getHandle(), key); } + public boolean isKeyDown(int key) { return (this.getKey(key) == GLFW_PRESS); } + public void postEmptyEvent() { glfwPostEmptyEvent(); } + + public long getHandle() { return this.handle; } + public Object getLock() { return this.lock; } + public boolean isDestroyed() { return this.destroyed; } + + public float getElapsedTime() { return this.elapsedTime; } + public int getTargetFps() { return this.targetFps; } public void setTargetFps(int targetFps) { this.targetFps = targetFps; } - public void setTargetUps(int targetFps) { this.targetUps = targetUps; } - + // width, height - public int getWidth() { return !this.isWindowed() ? this.getScreenWidth() : this.getWindowWidth(); } - public int getHeight() { return !this.isWindowed() ? this.getScreenHeight() : this.getWindowHeight(); } - public int getWindowWidth() { return this.width; } + public int getWidth() { return !this.isWindowed() ? this.getScreenWidth() : this.getWindowWidth(); } + public int getHeight() { return !this.isWindowed() ? this.getScreenHeight() : this.getWindowHeight(); } + public int getWindowWidth() { return this.width; } public int getWindowHeight() { return this.height; } - public int getScreenWidth() { return this.vidmode.width(); } + public int getScreenWidth() { return this.vidmode.width(); } public int getScreenHeight() { return this.vidmode.height(); } // xpos, ypos - public int getXPos() { return !this.isWindowed() ? this.getScreenXPos() : this.getWindowXPos(); } - public int getYPos() { return !this.isWindowed() ? this.getScreenYPos() : this.getWindowYPos(); } - public int getWindowXPos() { return this.xPos; } - public int getWindowYPos() { return this.yPos; } - public int getScreenXPos() { return 0; } - public int getScreenYPos() { return 0; } + public int getXPos() { return !this.isWindowed() ? this.getScreenXPos() : this.getWindowXPos(); } + public int getYPos() { return !this.isWindowed() ? this.getScreenYPos() : this.getWindowYPos(); } + public int getWindowXPos() { return this.xPos; } + public int getWindowYPos() { return this.yPos; } + public int getScreenXPos() { return 0; } + public int getScreenYPos() { return 0; } public int getCenteredXPos() { return !this.isWindowed() ? 0 : (this.getScreenWidth() - this.getWindowWidth()) / 2; } public int getCenteredYPos() { return !this.isWindowed() ? 0 : (this.getScreenHeight() - this.getWindowHeight()) / 2; } // setter @@ -294,9 +300,9 @@ public void setSize(int width, int height) { glViewport(0, 0, this.getWidth(), this.getHeight()); this.updateWindowMonitor(); } - - public int getMode() { return this.mode; } - public boolean isWindowed() { return this.mode == WINDOWED; } + + public int getMode() { return this.mode; } + public boolean isWindowed() { return this.mode == WINDOWED; } public boolean isBorderless() { return this.mode == BORDERLESS; } public boolean isFullscreen() { return this.mode == FULLSCREEN; } public void setMode(int mode) { @@ -309,7 +315,7 @@ public void setMode(int mode) { glViewport(0, 0, this.getWidth(), this.getHeight()); this.updateWindowMonitor(); } - + public boolean isVSync() { return this.vSync; } public void setVSync(boolean vSync) { this.vSync = vSync; diff --git a/src/game/Background.java b/src/game/Background.java index 210b45a..8638251 100644 --- a/src/game/Background.java +++ b/src/game/Background.java @@ -9,56 +9,53 @@ import static org.lwjgl.glfw.GLFW.*; -public class Background implements Scene { +public class Background implements Loopable { + public static final float COLOR_STEP = 0.3f; // how much colour changed in 1 second + private int direction; private float color; - - private Camera camera; - private int render; - - public Background(Camera camera) { + + public Background() { this.direction = 0; this.color = 0.5f; - this.camera = camera; - this.render = 0; } - + @Override - public void init(Window window) throws Exception { - // blank background for first frame - window.clearColor(1f, 1f, 1f, 0f); + public void init(Window window) { } - + @Override public void input(Window window) { + this.inputColor(window); + } + + public void inputColor(Window window) { if (window.isKeyDown(GLFW_KEY_L)) this.color = 0f; - + this.direction = 0; if (window.isKeyDown(GLFW_KEY_UP)) this.direction++; if (window.isKeyDown(GLFW_KEY_DOWN)) this.direction--; - - // render distance (camera) - this.render = 0; - if (window.isKeyDown(GLFW_KEY_L)) this.camera.setFar(Camera.FAR); - if (window.isKeyDown(GLFW_KEY_RIGHT_BRACKET)) this.render++; - if (window.isKeyDown(GLFW_KEY_LEFT_BRACKET)) this.render--; } - + @Override public void update(float interval) { - this.color = Math.max(0f, Math.min(1f, this.color+0.01f*this.direction)); - this.camera.setFar(Math.max(Camera.NEAR+0.01f, this.camera.getFar() + 0.1f*this.render)); + this.updateColor(interval); + } + + public void updateColor(float interval) { + this.color = Math.max(0f, Math.min(1f, this.color + this.direction * interval*COLOR_STEP)); } - + @Override public void render(Window window) { + this.updateBackground(window); + } + + public void updateBackground(Window window) { // Different color based on vSync or not (colorful = vSync on) if (window.isVSync()) window.clearColor(1-this.color, this.color/2+0.5f, this.color, 0.0f); else window.clearColor(this.color, this.color, this.color, 0.0f); } - - @Override - public void cleanup() {} } diff --git a/src/game/BlockItem.java b/src/game/BlockItem.java new file mode 100644 index 0000000..a677f16 --- /dev/null +++ b/src/game/BlockItem.java @@ -0,0 +1,34 @@ +/* +George Zhang +Block item subclass. +*/ + +package geetransit.minecraft05.game; + +import geetransit.minecraft05.engine.*; + +import org.joml.Vector3f; +import org.joml.Quaternionf; + +public class BlockItem extends Item { + private boolean selected; + private boolean visible; + + public BlockItem(Mesh mesh) { + super(mesh); + this.selected = false; + this.visible = false; + } + + public BlockItem setPosition(Vector3f position) { super.setPosition(position); return this; } + public BlockItem setPosition(float x, float y, float z) { super.setPosition(x, y, z); return this; } + public BlockItem setRotation(Quaternionf rotation) { super.setRotation(rotation); return this; } + public BlockItem setRotation(float x, float y, float z) { super.setRotation(x, y, z); return this; } + public BlockItem setScale(float scale) { super.setScale(scale); return this; } + + public boolean isSelected() { return this.selected; } + public BlockItem setSelected(boolean selected) { this.selected = selected; return this; } + + public boolean isVisible() { return this.visible; } + public BlockItem setVisible(boolean visible) { this.visible = visible; return this; } +} diff --git a/src/game/Game.java b/src/game/Game.java index f94c7f4..59675a8 100644 --- a/src/game/Game.java +++ b/src/game/Game.java @@ -14,61 +14,55 @@ import static org.lwjgl.glfw.GLFW.*; import static org.lwjgl.opengl.GL11.*; -public class Game extends SceneBase { +public class Game extends Scene { private Mouse mouse; private Camera camera; - + private Background background; private Skybox skybox; - private World world; - private Hud hud; - + private Player player; // player actions and hud + private World world; // world loading and rendering + public Game() { super(); + + // inputs this.mouse = new Mouse(); - this.camera = new Camera(); - - this.background = new Background(this.camera); + this.camera = new Camera(this.mouse); + + // child scenes + this.background = new Background(); this.skybox = new Skybox(this.camera); - this.world = new World(this.mouse, this.camera); - this.hud = new Hud(this.mouse, this.camera, this.world); - - // add scenes - this - .addScene(this.background) - .addScene(this.skybox) - .addScene(this.world) - .addScene(this.hud); + this.world = new World(this.camera); + this.player = new Player(this.mouse, this.camera, this.world); + + // scene ordering + this.addFrom(this.mouse); + this.addFrom(this.camera); + this.addFrom(this.background); + this.addFrom(this.skybox); + this.addFrom(this.world); + this.addFrom(this.player); } - - public Mouse getMouse() { return this.mouse; } - public Camera getCamera() { return this.camera; } - - public Background getBackground() { return this.background; } - public World getWorld() { return this.world; } - public Hud getHud() { return this.hud; } - + @Override - public void init(Window window) throws Exception { + public void init(Window window) { System.out.println("LWJGL version: " + Version.getVersion()); System.out.println("OpenGL version: " + GL11.glGetString(GL11.GL_VERSION)); - - // setup mouse - this.mouse.init(window); - + // call child scenes' init super.init(window); - + // Use correct depth checking glEnable(GL_DEPTH_TEST); - + // makes text better glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - + // Setup a key callback. It will be called every time a key is pressed, repeated or released. window.setKeyCallback((handle, key, scancode, action, mods) -> { - if (key == GLFW_KEY_F4 && action == GLFW_RELEASE && ((mods & GLFW_MOD_SHIFT) != 0)) { + if (key == GLFW_KEY_F4 && action == GLFW_RELEASE && ((mods & GLFW_MOD_ALT) != 0)) { window.setShouldClose(true); // We will detect this in the rendering loop window.postEmptyEvent(); } @@ -116,23 +110,23 @@ else if ((mods & GLFW_MOD_SHIFT) != 0) window.next.add("targetFps", () -> window.setTargetFps(window.getTargetFps() + 1)); // debug if (key == GLFW_KEY_U && action == GLFW_RELEASE) { - System.out.println(this.camera); - System.out.println(this.mouse); + System.out.println("mouse="+this.mouse); + System.out.println("camera="+this.camera); } }); } - - @Override - public void input(Window window) { - this.mouse.input(window); - super.input(window); - } - + @Override public void render(Window window) { // clear the framebuffer window.clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - + + // build matrices + window.buildProjectionMatrix(this.camera); + window.buildOrthoProjectionMatrix(); + this.camera.buildViewMatrix(); + this.camera.updateFrustum(window.getProjectionMatrix()); + // render scenes super.render(window); } diff --git a/src/game/Hud.java b/src/game/Hud.java deleted file mode 100644 index 5ad57a0..0000000 --- a/src/game/Hud.java +++ /dev/null @@ -1,82 +0,0 @@ -/* -ahbejarano -Hud implementation. -*/ - -package geetransit.minecraft05.game; - -import geetransit.minecraft05.engine.*; - -import static org.lwjgl.glfw.GLFW.*; - -public class Hud extends SceneRender { - private static final int FONT_COLS = 16; - private static final int FONT_ROWS = 16; - private static final String FONT_FILE = "/res/font.png"; - - private TextItem text; - private Item compass; - private Item crosshair; - private Mouse mouse; - private Camera camera; - private World world; - - public Hud(Mouse mouse, Camera camera, World world) { - super(); - this.setRenderer(new Renderer() { - Shader shader; - public void init(Window window) throws Exception { - shader = create2D("/res/vertex-2d.vs", "/res/fragment-2d.fs"); - } - public void render(Window window) { - render2D(shader, window, Hud.this.getItems()); - } - public void cleanup() { - shader.cleanup(); - } - }); - this.mouse = mouse; - this.camera = camera; - this.world = world; - } - - @Override - public void init(Window window) throws Exception { - super.init(window); - - this.text = new TextItem("", FONT_FILE, FONT_COLS, FONT_ROWS); - this.text.getMesh().setColor(1, 1, 1); - - this.compass = new Item(ObjLoader.loadMesh("/res/compass.obj")); - this.compass.getMesh().setColor(1, 1, 1); - - this.crosshair = new Item(ObjLoader.loadMesh("/res/crosshair.obj")); - this.crosshair.getMesh().setColor(1, 1, 1); - - this - .addItem(this.text) - .addItem(this.compass) - .addItem(this.crosshair); - } - - @Override - public void render(Window window) { - this.text.setText(String.format( - "vsync=%s mode=%s mouse=%s\nchange=%s wait=%s\ncamera=%s\nmouse=%s", - window.isVSync(), window.getMode(), window.getInputMode(GLFW_CURSOR) == GLFW_CURSOR_NORMAL, - this.world.getChange(), this.world.getWait(), - this.camera, this.mouse - )); - this.text.setPosition(10f, window.getHeight() * 0.85f, 0f); - this.text.setScale(window.getWidth() * (1/3500f)); - - this.compass.setPosition(window.getWidth() * 0.95f, window.getWidth() * 0.05f, 0f); - this.compass.setRotation(0f, 0f, 180f - this.camera.getRotation().y); - this.compass.setScale(window.getWidth() * (1/20f)); - - this.crosshair.setPosition(window.getWidth() * 0.5f, window.getHeight() * 0.5f, 0f); - this.crosshair.setScale(window.getWidth() * (1/50f)); - - super.render(window); - } -} diff --git a/src/game/Player.java b/src/game/Player.java new file mode 100644 index 0000000..b40a896 --- /dev/null +++ b/src/game/Player.java @@ -0,0 +1,208 @@ +/* +George Zhang +Player interaction class. +*/ + +package geetransit.minecraft05.game; + +import geetransit.minecraft05.engine.*; + +import java.util.*; +import java.util.function.BiConsumer; +import org.joml.Vector3f; +import org.joml.Matrix4f; + +import static org.lwjgl.glfw.GLFW.*; +import static org.lwjgl.opengl.GL11.*; + +public class Player implements Loopable, AutoCloseable { + public static final float CHANGE_DELAY = 0.2f; // time between block change (place / remove) + public static final float MOVEMENT_STEP = 3.0f; // distance moved in 1 second + public static final float SPRINT_MULTIPLIER = 1.5f; // sprinting change + + public static final int FONT_COLS = 16; + public static final int FONT_ROWS = 16; + public static final String FONT_FILE = "/res/font.png"; + + private final Mouse mouse; + private final Camera camera; + private final World world; + private Window window; + + private final Countdown countdown; + private final Vector3f movement; + private String change; // ""=air + + private Shader shader; + private List items; + + private TextItem text; + private Item compass; + private Item crosshair; + + public Player(Mouse mouse, Camera camera, World world) { + this.mouse = mouse; + this.camera = camera; + this.world = world; + + this.countdown = new Countdown(CHANGE_DELAY); + this.movement = new Vector3f(); + this.items = new ArrayList<>(); + } + + @Override + public void init(Window window) { + this.window = window; + this.createShader(); + this.createHud(); + } + + public void createShader() { + this.shader = new Shader(); + this.shader.compileVertex(Utils.loadResource("/res/vertex-2d.vs")); + this.shader.compileFragment(Utils.loadResource("/res/fragment-2d.fs")); + this.shader.link(); + + this.shader.create("projModelMatrix"); + this.shader.create("texture_sampler"); + this.shader.create("color"); + this.shader.create("isTextured"); + } + + public void createHud() { + this.text = new TextItem("", new FontTexture(FONT_FILE, FONT_COLS, FONT_ROWS)); + this.text.getMesh().setColor(1, 1, 1); + + this.compass = new Item(ObjLoader.loadMesh("/res/compass.obj")); + this.compass.getMesh().setColor(1, 1, 1); + + this.crosshair = new Item(ObjLoader.loadMesh("/res/crosshair.obj")); + this.crosshair.getMesh().setColor(1, 1, 1); + + this.items.add(this.text); + this.items.add(this.compass); + this.items.add(this.crosshair); + } + + @Override + public void input(Window window) { + this.inputMovement(window); + this.inputPlace(window); + } + + public void inputMovement(Window window) { + // movement + this.movement.zero(); + boolean SPRINTING = !window.isKeyDown(GLFW_KEY_LEFT_SHIFT) && window.isKeyDown(GLFW_KEY_LEFT_CONTROL); + + if (window.isKeyDown(GLFW_KEY_W)) this.movement.z--; + if (window.isKeyDown(GLFW_KEY_S)) this.movement.z++; + if (window.isKeyDown(GLFW_KEY_A)) this.movement.x--; + if (window.isKeyDown(GLFW_KEY_D)) this.movement.x++; + + if (window.isKeyDown(GLFW_KEY_LEFT_SHIFT)) this.movement.y--; + if (window.isKeyDown(GLFW_KEY_SPACE)) this.movement.y++; + + if (this.movement.length() > 1f) this.movement.div(this.movement.length()); + if (SPRINTING && this.movement.z < 0) this.movement.mul(SPRINT_MULTIPLIER); + } + + public void inputPlace(Window window) { + // placing / removing + this.change = null; + if (window.isKeyDown(GLFW_KEY_0)) this.change = ""; + if (window.isKeyDown(GLFW_KEY_1)) this.change = "grassblock"; + if (window.isKeyDown(GLFW_KEY_2)) this.change = "cobbleblock"; + if (window.isKeyDown(GLFW_KEY_3)) this.change = "glassblock"; + if (this.change == null) this.countdown.reset(); + } + + @Override + public void update(float interval) { + this.updateMovement(interval); + this.updatePlace(interval); + this.updateHud(this.window); + } + + public void updateMovement(float interval) { + // movement + this.camera.movePosition(this.movement, interval*MOVEMENT_STEP); + } + + public void updatePlace(float interval) { + // placing / removing + this.countdown.add(interval); + if (this.change != null && this.countdown.nextOnce()) { + ClosestItem closest = this.world.updateClosest(); + // check if block found + if (closest.closest != null) { + Vector3f position = new Vector3f(); + position.set(closest.direction); // get normalized camera direction + position.negate(); // move towards camera + position.mul(0.001f * (this.change.equals("") ? -1 : 1)); // go to block + position.add(closest.hit); // start from intersection point + position.round(); // round to grid + this.world.setBlock(this.change, position); + } + } + } + + public void updateHud(Window window) { + this.text.setPosition(10f, window.getHeight() * 0.85f, 0f); + this.text.setScale(window.getWidth() * (1/3500f)); + this.text.setText(String.format( + "vsync=%s mode=%s mouse=%s\nchange=%s wait=%s\ncamera=%s\nmouse=%s", + window.isVSync(), window.getMode(), window.getInputMode(GLFW_CURSOR) == GLFW_CURSOR_NORMAL, + this.change, Math.max(0, this.countdown.getWait()), + this.camera, this.mouse + )); + + this.compass.setPosition(window.getWidth() * 0.95f, window.getWidth() * 0.05f, 0f); + this.compass.setRotation(0f, 0f, 180f - this.camera.getRotation().y); + this.compass.setScale(window.getWidth() * (1/20f)); + + this.crosshair.setPosition(window.getWidth() * 0.5f, window.getHeight() * 0.5f, 0f); + this.crosshair.setScale(window.getWidth() * (1/50f)); + } + + @Override + public void render(Window window) { + this.renderHud(window); + } + + public void renderHud(Window window) { + this.shader.bind(); + this.shader.set("texture_sampler", 0); + + // disable depth testing : source # https://stackoverflow.com/a/5467636 + glDepthMask(false); // disable writes to Z-Buffer + glDisable(GL_DEPTH_TEST); // disable depth-testing + + Matrix4f orthoMatrix = window.getOrthoProjectionMatrix(); + Matrix4f temp = new Matrix4f(); + BiConsumer setup = (shader, item) -> { + item.buildOrthoProjModelMatrix(orthoMatrix, temp); + shader.set("projModelMatrix", temp); + }; + + // draw items + for (Item item : this.items) + item.getMesh().render(this.shader, item, setup); + + glDepthMask(true); + glEnable(GL_DEPTH_TEST); + this.shader.unbind(); + } + + @Override + public void cleanup() { + this.close(); + } + + @Override + public void close() { + this.shader.close(); + for (Item item : this.items) + item.getMesh().close(); + } +} diff --git a/src/game/Skybox.java b/src/game/Skybox.java index 618b1af..26f4b3d 100644 --- a/src/game/Skybox.java +++ b/src/game/Skybox.java @@ -7,41 +7,139 @@ import geetransit.minecraft05.engine.*; -public class Skybox extends SceneRender { +import java.util.*; +import org.joml.Vector3f; +import org.joml.Matrix4f; + +import static org.lwjgl.glfw.GLFW.*; +import static org.lwjgl.opengl.GL11.*; + +public class Skybox implements Loopable, AutoCloseable { + public static final float RENDER_STEP = 3.0f; // render changed in 1 second + public static final float RENDER_MIN = Math.min(Camera.NEAR, 5f); // render changed in 1 second + public static final float RENDER_DELAY = 0.5f; // time between skybox toggling + public static final float SKYBOX_SCALE = 0.5f; // skybox scale (multiplied with camera far) + private Camera camera; + private Countdown countdown; + + private Shader shader; private Item skybox; - public Skybox(Camera camera) { - super(); - this.setRenderer(new Renderer() { - Shader shader; - public void init(Window window) throws Exception { - shader = create3D("/res/vertex-3d.vs", "/res/fragment-3d.fs"); - } - public void render(Window window) { - renderSkybox(shader, window, Skybox.this.getCamera(), Skybox.this.getItems()); - } - public void cleanup() { - shader.cleanup(); - } - }); + private boolean toggle; + private boolean visible; + private int render; + + public Skybox(Camera camera) { this.camera = camera; - } - - public Camera getCamera() { return this.camera; } - + this.countdown = new Countdown(RENDER_DELAY); + this.visible = true; + } + + @Override + public void init(Window window) { + this.createShader(); + this.createSkybox("/res/skybox.obj", "/res/skybox.png"); + } + + public void createShader() { + this.shader = new Shader(); + this.shader.compileVertex(Utils.loadResource("/res/vertex-3d.vs")); + this.shader.compileFragment(Utils.loadResource("/res/fragment-3d.fs")); + this.shader.link(); + + this.shader.create("projectionMatrix"); + this.shader.create("modelViewMatrix"); + this.shader.create("texture_sampler"); + this.shader.create("color"); + this.shader.create("isTextured"); + } + + public void createSkybox(String obj, String texture) { + this.skybox = new Item(ObjLoader.loadMesh(obj, texture)); + this.skybox.setPosition(0, 0, 0); + } + + @Override + public void input(Window window) { + this.inputSkybox(window); + } + + public void inputSkybox(Window window) { + // render distance (camera) + this.render = 0; + if (window.isKeyDown(GLFW_KEY_L)) this.camera.setFar(Camera.FAR); + if (window.isKeyDown(GLFW_KEY_RIGHT_BRACKET)) this.render++; + if (window.isKeyDown(GLFW_KEY_LEFT_BRACKET)) this.render--; + + // toggle skybox + this.toggle = window.isKeyDown(GLFW_KEY_T); + if (!this.toggle) + this.countdown.reset(); + } + @Override - public void init(Window window) throws Exception { - super.init(window); - Mesh mesh = ObjLoader.loadMesh("/res/skybox.obj"); - mesh.setTexture(new Texture("/res/skybox.png")); - this.skybox = new Item(mesh).setPosition(0, 0, 0); - this.addItem(this.skybox); - } - + public void update(float interval) { + this.updateSkybox(interval); + } + + public void updateSkybox(float interval) { + // render distance + this.camera.setFar(Math.max(RENDER_MIN, this.camera.getFar() + this.render * interval*RENDER_STEP)); + + // toggle skybox + this.countdown.add(interval); + if (this.toggle && this.countdown.nextOnce()) + this.visible = !this.visible; + } + @Override public void render(Window window) { - this.skybox.setScale(this.camera.getFar() * 0.5f); - super.render(window); + if (!this.visible) + return; + this.renderSkybox(window); + } + + public void renderSkybox(Window window) { + if (this.skybox == null) + return; + + this.shader.bind(); + this.shader.set("texture_sampler", 0); + this.shader.set("projectionMatrix", window.getProjectionMatrix()); + + // disable depth testing (very back) + glDepthMask(false); + glDisable(GL_DEPTH_TEST); + + // remove view translation + Matrix4f viewMatrix = this.camera.getViewMatrix(); + Vector3f oldTanslation = viewMatrix.getTranslation(new Vector3f()); + viewMatrix.setTranslation(0, 0, 0); + + // draw skybox + Matrix4f temp = new Matrix4f(); + this.skybox.getMesh().render(this.shader, this.skybox, (shader, item) -> { + item.buildModelViewMatrix(viewMatrix, temp); + shader.set("modelViewMatrix", temp); + }); + + // undo view translation removal + viewMatrix.setTranslation(oldTanslation); + + glDepthMask(true); + glEnable(GL_DEPTH_TEST); + this.shader.unbind(); + } + + @Override + public void cleanup() { + this.close(); + } + + @Override + public void close() { + this.shader.close(); + this.skybox.getMesh().close(); } } diff --git a/src/game/World.java b/src/game/World.java index 27c8bfe..cea9207 100644 --- a/src/game/World.java +++ b/src/game/World.java @@ -1,6 +1,6 @@ /* George Zhang -World scene implementation. +World view class. */ package geetransit.minecraft05.game; @@ -8,178 +8,249 @@ import geetransit.minecraft05.engine.*; import java.util.*; - +import java.util.function.*; +import java.util.stream.*; import org.joml.Vector3f; +import org.joml.Matrix4f; import static org.lwjgl.glfw.GLFW.*; +import static org.lwjgl.opengl.GL11.*; -public class World extends SceneRender { - private Mouse mouse; - private float sensitivity; - - private Camera camera; - private float step; - private Vector3f movement; - private ClosestItem closestItem; - - private Map blockMap; - - private static final float CHANGE_DELAY = 0.2f; - private String change; // ""=air - private float wait; // time until next place / remove - - private static final int MAX_COLOR = 255*255*255; - - public World(Mouse mouse, Camera camera) { - super(); - this.setRenderer(new Renderer() { - Shader shader; - public void init(Window window) throws Exception { - shader = create3D("/res/vertex-3d.vs", "/res/fragment-3d.fs"); - } - public void render(Window window) { - render3DMap(shader, window, World.this.getCamera(), World.this.getMeshMap()); - } - public void cleanup() { - shader.cleanup(); - } - }); - - this.mouse = mouse; - this.sensitivity = 0.3f; - +public class World implements Loopable, AutoCloseable { + public static final float BLOCK_SCALE = 0.5f; // block scaling (mesh is 2x2x2) + public static final float BLOCK_RADIUS = 2f; // radius around block (for frustum culling) + + private Shader shader; + private final Camera camera; + private final ClosestItem closest; + + private final Map> blocks; // type -> list + private final Map meshes; // type -> mesh + private final Set transparentNames; // type (if exists, transparent) + private final List transparentBlocks; // list (ordered) + + public World(Camera camera) { this.camera = camera; - this.step = 0.1f; - this.movement = new Vector3f(); - this.closestItem = new ClosestItem(); - } - - public Mouse getMouse() { return this.mouse; } - public float getSensitivity() { return this.sensitivity; } - - public Camera getCamera() { return this.camera; } - public float getStep() { return this.step; } - public Vector3f getMovement() { return this.movement; } - public ClosestItem getClosestItem() { return this.closestItem; } - - public String getChange() { return this.change; } - public float getWait() { return this.wait; } - + this.closest = new ClosestItem<>(); + + this.blocks = new HashMap<>(); + this.meshes = new HashMap<>(); + this.transparentNames = new HashSet<>(); + this.transparentBlocks = new ArrayList<>(); + } + @Override - public void init(Window window) throws Exception { - super.init(window); - - // Create the blocks' mesh - this.blockMap = new HashMap<>(); - this.blockMap.put("grassblock", this.loadBlock("/res/cube.obj", "/res/grassblock.png")); - this.blockMap.put("cobbleblock", this.loadBlock("/res/cube.obj", "/res/cobbleblock.png")); - - // get heightmap - try (HeightMap map = HeightMap.loadFromImage("/res/heightmap.png")) { - // create terrain - for (int x = 0; x < map.width; x++) { - for (int z = 0; z < map.length; z++) { - int y = (int) map.compressExpand(map.heightAt(x, z), 0, MAX_COLOR, 0, 16); - this.addItem(this.newBlock("grassblock").setPosition(x, y, z)); - for (int k = y-1; k >= Math.max(y-2, 0); k--) { - this.addItem(this.newBlock("cobbleblock").setPosition(x, k, z)); - } - } - } + public void init(Window window) { + this.createShader(); + + // add block types + this.addFullType("grassblock", ObjLoader.loadMesh("/res/cube-fblr,u,d.obj", "/res/grassblock.png")); + this.addFullType("cobbleblock", ObjLoader.loadMesh("/res/cube-fblrud.obj", "/res/cobbleblock.png")); + this.addTransparentType("glassblock", ObjLoader.loadMesh("/res/cube-fblrud.obj", "/res/glassblock.png")); + + // build world + this.buildSpawn("grassblock", "cobbleblock"); + try (Image image = new Image("/res/heightmap.png")) { + // heightmap + this.buildSimple(image, "grassblock", "cobbleblock"); } - - // add spawn markers (-2z is forwards) - this - .addItem(this.newBlock("grassblock").setPosition(+1, +1, 0)) - .addItem(this.newBlock("grassblock").setPosition(-1, +1, 0)) - .addItem(this.newBlock("grassblock").setPosition( 0, +1, +1)) - .addItem(this.newBlock("grassblock").setPosition( 0, +1, -2)); } - @Override - public void input(Window window) { - super.input(window); - - // movement - this.movement.zero(); - boolean sprinting = (!window.isKeyDown(GLFW_KEY_LEFT_SHIFT) && window.isKeyDown(GLFW_KEY_LEFT_CONTROL)); - - if (window.isKeyDown(GLFW_KEY_W)) this.movement.z--; - if (window.isKeyDown(GLFW_KEY_S)) this.movement.z++; - if (window.isKeyDown(GLFW_KEY_A)) this.movement.x--; - if (window.isKeyDown(GLFW_KEY_D)) this.movement.x++; - - if (window.isKeyDown(GLFW_KEY_LEFT_SHIFT)) this.movement.y--; - if (window.isKeyDown(GLFW_KEY_SPACE)) this.movement.y++; - - if (this.movement.length() > 1f) this.movement.div(this.movement.length()); - if (sprinting && this.movement.z < 0) this.movement.mul(1.5f); - - // placing / removing - this.change = null; - if (window.isKeyDown(GLFW_KEY_0)) this.change = ""; - if (window.isKeyDown(GLFW_KEY_1)) this.change = "grassblock"; - if (window.isKeyDown(GLFW_KEY_2)) this.change = "cobbleblock"; - } - @Override public void update(float interval) { - super.update(interval); - - // movement - if (this.mouse.isInside()) - this.camera.rotateUsingMouse(this.mouse, this.sensitivity); - this.camera.movePosition(this.movement.mul(this.step, new Vector3f())); - - // placing / removing - if (this.change != null && this.wait <= 0) { - this.closestItem.update(this.getItems(), this.camera); - if (this.closestItem.closest != null) { - if (this.change.equals("")) { - this.removeItem(this.closestItem.closest); - } else { - Vector3f position = new Vector3f(); - position.set(this.closestItem.direction); // get normalized camera direction - position.negate(); // move towards camera - position.mul(0.01f); - position.add(this.closestItem.hit); // start from intersection point - position.round(); // round to grid - this.addItem(this.newBlock(this.change).setPosition(position)); - } + // best to leave it up to others whether they want to update or not + } + + @Override + public void render(Window window) { + this.updateTransparent(); + this.updateVisible(); + this.updateSelected(); + this.renderWorld(window); + } + + @Override + public void cleanup() { + this.close(); + } + + public void createShader() { + this.shader = new Shader(); + this.shader.compileVertex(Utils.loadResource("/res/vertex-3d.vs")); + this.shader.compileFragment(Utils.loadResource("/res/fragment-3d-block.fs")); + this.shader.link(); + + this.shader.create("projectionMatrix"); + this.shader.create("modelViewMatrix"); + this.shader.create("texture_sampler"); + this.shader.create("color"); + this.shader.create("isTextured"); + this.shader.create("isSelected"); + } + + public void buildSimple(Image image, String top, String bottom) { + // create terrain + // T + // T B + // T B B + // B B B + // B B + for (int x = 0; x < image.width; x++) { + for (int z = 0; z < image.length; z++) { + int y = (int) Image.compressExpand(image.pixel(x, z), 0, Image.MAX, 0, 16); + this.setBlock(top, x, y, z); + for (int k = y-1; k >= Math.max(y-2, 0); k--) + this.setBlock(bottom, x, k, z); } - this.wait += this.CHANGE_DELAY; } - - // update wait time - if (this.wait > 0) - this.wait -= interval; - if (this.wait < 0 && this.change == null) - this.wait = 0; - } - + } + + public void buildSpawn(String normal, String left) { + // add spawn markers (-2z is forwards, cobble is left) + // N + // + // L N + // N + this.setBlock(normal, +1, 0, 0); + this.setBlock(left, -1, 0, 0); + this.setBlock(normal, 0, 0, +1); + this.setBlock(normal, 0, 0, -2); + } + + public void updateTransparent() { + // reorder transparent blocks + Vector3f position = this.camera.getPosition(); + this.transparentBlocks.sort(Comparator.comparingDouble( + block -> -block.getPosition().distance(position) + )); + } + + public void updateVisible() { + // update visible blocks + for (BlockItem block : this.iterBlocks()) + block.setVisible(this.camera.insideFrustum(block.getPosition(), BLOCK_RADIUS*block.getScale())); + } + + public void updateSelected() { + // update selected block + for (BlockItem block : this.iterBlocks()) + block.setSelected(false); + this.closest.update(this.iterBlocks(), this.camera); + if (this.closest.closest != null) + this.closest.closest.setSelected(true); + } + + public void renderWorld(Window window) { + this.shader.bind(); + this.shader.set("texture_sampler", 0); + this.shader.set("projectionMatrix", window.getProjectionMatrix()); + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); + + Matrix4f viewMatrix = this.camera.getViewMatrix(); // view matrix + Matrix4f temp = new Matrix4f(); // temporary matrix (stores model view matrix) + BiConsumer setup = (shader, block) -> { + block.buildModelViewMatrix(viewMatrix, temp); + shader.set("modelViewMatrix", temp); + shader.set("isSelected", block.isSelected()); + }; + + // opaque blocks (loop by type) + for (Map.Entry> entry : this.blocks.entrySet()) { + Mesh mesh = this.meshes.get(entry.getKey()); + Stream blocks = entry.getValue().stream().filter(block -> block.isVisible()); + mesh.render(this.shader, blocks, setup); + } + + // transparent blocks (loop individually) + for (BlockItem block : this.transparentBlocks) { + Mesh mesh = block.getMesh(); + if (block.isVisible()) + mesh.render(this.shader, block, setup); + } + + glDisable(GL_CULL_FACE); + this.shader.unbind(); + } + @Override - public void render(Window window) { - this.updateSelectedItem(); - super.render(window); - } - - private void updateSelectedItem() { - for (Item item : this.getItems()) - item.setSelected(false); - this.closestItem.update(this.getItems(), this.camera); - if (this.closestItem.closest != null) - this.closestItem.closest.setSelected(true); - } - - private static Item loadBlock(String objFileName, String textureFileName) throws Exception { - Mesh mesh = ObjLoader.loadMesh(objFileName); - mesh.setTexture(new Texture(textureFileName)); - Item block = new Item(mesh); - block.setScale(0.5f); - return block; - } - - private Item newBlock(String name) { - return this.blockMap.get(name).clone(); + public void close() { + this.shader.close(); + for (Mesh mesh : this.meshes.values()) + mesh.close(); + } + + private Iterable iterBlocks() { + return this.streamBlocks()::iterator; + } + + private Stream streamBlocks() { + Stream blocks = this.blocks.values().stream().flatMap(Collection::stream); + Stream transparentBlocks = this.transparentBlocks.stream(); + return Stream.concat(blocks, transparentBlocks); + } + + public ClosestItem updateClosest() { + this.closest.update(this.iterBlocks(), this.camera); + return this.closest; + } + + public void addFullType(String name, Mesh mesh) { + if (this.meshes.containsKey(name)) + throw new RuntimeException("type already defined: "+name); + this.meshes.put(name, mesh); + this.blocks.put(name, new ArrayList<>()); + } + + public void addTransparentType(String name, Mesh mesh) { + if (this.meshes.containsKey(name)) + throw new RuntimeException("type already defined: "+name); + this.meshes.put(name, mesh); + this.transparentNames.add(name); + } + + public void setBlock(String name, Vector3f position) { this.setBlock(name, position.x, position.y, position.z); } + public void setBlock(String name, float x, float y, float z) { + this.removeBlock(x, y, z); + if (name.length() != 0) + this.addBlock(name, x, y, z); + } + + private void addBlock(String name, float x, float y, float z) { + BlockItem block = new BlockItem(this.meshes.get(name)); + block.setScale(BLOCK_SCALE); + block.setPosition(x, y, z); + this.addBlock(block); + } + + private void addBlock(BlockItem block) { + String name = this.nameOf(block.getMesh()); + if (this.transparentNames.contains(name)) + this.transparentBlocks.add(block); + else + this.blocks.get(name).add(block); + } + + private void removeBlock(float x, float y, float z) { + for (BlockItem block : this.iterBlocks()) + if (block.getPosition().equals(x, y, z)) { + this.removeBlock(block); + return; + } + } + + private void removeBlock(BlockItem block) { + String name = this.nameOf(block.getMesh()); + if (this.transparentNames.contains(name)) + this.transparentBlocks.remove(block); + else + this.blocks.get(name).remove(block); + } + + private String nameOf(Mesh mesh) { + for (Map.Entry entry : this.meshes.entrySet()) + if (entry.getValue() == mesh) + return entry.getKey(); + throw new RuntimeException("could not find name of mesh"); } }