diff --git a/model-viewer/pom.xml b/model-viewer/pom.xml
new file mode 100644
index 0000000000..a52ce360d7
--- /dev/null
+++ b/model-viewer/pom.xml
@@ -0,0 +1,96 @@
+
+
+
+ 4.0.0
+
+
+ net.runelite
+ runelite-parent
+ 1.1.0-SNAPSHOT
+
+
+ net.runelite
+ modelviewer
+ 1.0.0-SNAPSHOT
+ Model Viewer
+
+
+
+ net.runelite
+ deob
+ 1.1.0-SNAPSHOT
+
+
+
+ org.lwjgl.lwjgl
+ lwjgl
+ 2.9.3
+
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.12
+
+
+ org.slf4j
+ slf4j-simple
+ 1.7.12
+
+
+
+ junit
+ junit
+ 4.12
+ test
+
+
+
+
+
+
+ com.googlecode.mavennatives
+ maven-nativedependencies-plugin
+ 0.0.6
+
+
+ unpacknatives
+ generate-resources
+
+
+ copy
+
+
+
+
+
+
+
diff --git a/model-viewer/src/main/java/net/runelite/modelviewer/Camera.java b/model-viewer/src/main/java/net/runelite/modelviewer/Camera.java
new file mode 100644
index 0000000000..1a25de1a31
--- /dev/null
+++ b/model-viewer/src/main/java/net/runelite/modelviewer/Camera.java
@@ -0,0 +1,218 @@
+package net.runelite.modelviewer;
+
+import org.lwjgl.input.Keyboard;
+import org.lwjgl.input.Mouse;
+import static org.lwjgl.opengl.GL11.glRotatef;
+import static org.lwjgl.opengl.GL11.glTranslatef;
+
+public class Camera {
+ public static float moveSpeed = 0.05f;
+
+ private static float maxLook = 85;
+
+ private static float mouseSensitivity = 0.05f;
+
+ private static Vector3f pos;
+ private static Vector3f rotation;
+
+ public static void create() {
+ pos = new Vector3f(0, 0, 0);
+ rotation = new Vector3f(0, 0, 0);
+ }
+
+ public static void apply() {
+ if(rotation.y / 360 > 1) {
+ rotation.y -= 360;
+ } else if(rotation.y / 360 < -1) {
+ rotation.y += 360;
+ }
+ // glLoadIdentity();
+ glRotatef(rotation.x, 1, 0, 0);
+ glRotatef(rotation.y, 0, 1, 0);
+ glRotatef(rotation.z, 0, 0, 1);
+ glTranslatef(-pos.x, -pos.y, -pos.z);
+ }
+
+ public static void acceptInput(float delta) {
+ acceptInputRotate(delta);
+ acceptInputGrab();
+ acceptInputMove(delta);
+ }
+
+ public static void acceptInputRotate(float delta) {
+ if(Mouse.isGrabbed()) {
+ float mouseDX = Mouse.getDX();
+ float mouseDY = -Mouse.getDY();
+ rotation.y += mouseDX * mouseSensitivity * delta;
+ rotation.x += mouseDY * mouseSensitivity * delta;
+ rotation.x = Math.max(-maxLook, Math.min(maxLook, rotation.x));
+ }
+ }
+
+ public static void acceptInputGrab() {
+ if(Mouse.isInsideWindow() && Mouse.isButtonDown(0)) {
+ Mouse.setGrabbed(true);
+ }
+ if(Keyboard.isKeyDown(Keyboard.KEY_ESCAPE)) {
+ Mouse.setGrabbed(false);
+ }
+ }
+
+ public static void acceptInputMove(float delta) {
+ boolean keyUp = Keyboard.isKeyDown(Keyboard.KEY_W);
+ boolean keyDown = Keyboard.isKeyDown(Keyboard.KEY_S);
+ boolean keyRight = Keyboard.isKeyDown(Keyboard.KEY_D);
+ boolean keyLeft = Keyboard.isKeyDown(Keyboard.KEY_A);
+ boolean keyFast = Keyboard.isKeyDown(Keyboard.KEY_Q);
+ boolean keySlow = Keyboard.isKeyDown(Keyboard.KEY_E);
+ boolean keyFlyUp = Keyboard.isKeyDown(Keyboard.KEY_SPACE);
+ boolean keyFlyDown = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT);
+
+
+ float speed;
+
+ if(keyFast) {
+ speed = moveSpeed * 5;
+ }
+ else if(keySlow) {
+ speed = moveSpeed / 2;
+ }
+ else {
+ speed = moveSpeed;
+ }
+
+ speed *= delta;
+
+ if(keyFlyUp) {
+ pos.y += speed;
+ }
+ if(keyFlyDown) {
+ pos.y -= speed;
+ }
+
+ if(keyDown) {
+ pos.x -= Math.sin(Math.toRadians(rotation.y)) * speed;
+ pos.z += Math.cos(Math.toRadians(rotation.y)) * speed;
+ }
+ if(keyUp) {
+ pos.x += Math.sin(Math.toRadians(rotation.y)) * speed;
+ pos.z -= Math.cos(Math.toRadians(rotation.y)) * speed;
+ }
+ if(keyLeft) {
+ pos.x += Math.sin(Math.toRadians(rotation.y - 90)) * speed;
+ pos.z -= Math.cos(Math.toRadians(rotation.y - 90)) * speed;
+ }
+ if(keyRight) {
+ pos.x += Math.sin(Math.toRadians(rotation.y + 90)) * speed;
+ pos.z -= Math.cos(Math.toRadians(rotation.y + 90)) * speed;
+ }
+ }
+
+ public static void setSpeed(float speed) {
+ moveSpeed = speed;
+ }
+
+ public static void setPos(Vector3f pos) {
+ Camera.pos = pos;
+ }
+
+ public static Vector3f getPos() {
+ return pos;
+ }
+
+ public static void setX(float x) {
+ pos.x = x;
+ }
+
+ public static float getX() {
+ return pos.x;
+ }
+
+ public static void addToX(float x) {
+ pos.x += x;
+ }
+
+ public static void setY(float y) {
+ pos.y = y;
+ }
+
+ public static float getY() {
+ return pos.y;
+ }
+
+ public static void addToY(float y) {
+ pos.y += y;
+ }
+
+ public static void setZ(float z) {
+ pos.z = z;
+ }
+
+ public static float getZ() {
+ return pos.z;
+ }
+
+ public static void addToZ(float z) {
+ pos.z += z;
+ }
+
+ public static void setRotation(Vector3f rotation) {
+ Camera.rotation = rotation;
+ }
+
+ public static Vector3f getRotation() {
+ return rotation;
+ }
+
+ public static void setRotationX(float x) {
+ rotation.x = x;
+ }
+
+ public static float getRotationX() {
+ return rotation.x;
+ }
+
+ public static void addToRotationX(float x) {
+ rotation.x += x;
+ }
+
+ public static void setRotationY(float y) {
+ rotation.y = y;
+ }
+
+ public static float getRotationY() {
+ return rotation.y;
+ }
+
+ public static void addToRotationY(float y) {
+ rotation.y += y;
+ }
+
+ public static void setRotationZ(float z) {
+ rotation.z = z;
+ }
+
+ public static float getRotationZ() {
+ return rotation.z;
+ }
+
+ public static void addToRotationZ(float z) {
+ rotation.z += z;
+ }
+
+ public static void setMaxLook(float maxLook) {
+ Camera.maxLook = maxLook;
+ }
+
+ public static float getMaxLook() {
+ return maxLook;
+ }
+
+ public static void setMouseSensitivity(float mouseSensitivity) {
+ Camera.mouseSensitivity = mouseSensitivity;
+ }
+
+ public static float getMouseSensitivity() {
+ return mouseSensitivity;
+ }
+}
\ No newline at end of file
diff --git a/model-viewer/src/main/java/net/runelite/modelviewer/ModelViewer.java b/model-viewer/src/main/java/net/runelite/modelviewer/ModelViewer.java
new file mode 100644
index 0000000000..096e46d601
--- /dev/null
+++ b/model-viewer/src/main/java/net/runelite/modelviewer/ModelViewer.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (c) 2016, Adam
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. All advertising materials mentioning features or use of this software
+ * must display the following acknowledgement:
+ * This product includes software developed by Adam
+ * 4. Neither the name of the Adam nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY Adam ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL Adam BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.runelite.modelviewer;
+
+import java.awt.Color;
+import java.io.File;
+import java.nio.file.Files;
+import net.runelite.cache.definitions.loaders.ModelLoader;
+import org.lwjgl.opengl.Display;
+import org.lwjgl.opengl.DisplayMode;
+import org.lwjgl.opengl.GL11;
+
+public class ModelViewer
+{
+ public static void main(String[] args) throws Exception
+ {
+ if (args.length < 1)
+ {
+ System.err.println("Usage: modelfile");
+ System.exit(1);
+ }
+
+ ModelLoader md = new ModelLoader();
+ byte[] b = Files.readAllBytes(new File(args[0]).toPath());
+ md.load(b);
+
+ Display.setDisplayMode(new DisplayMode(800, 600));
+ Display.setTitle("Model Viewer");
+ Display.setInitialBackground((float) Color.gray.getRed() / 255f, (float) Color.gray.getGreen() / 255f, (float) Color.gray.getBlue() / 255f);
+ Display.create();
+
+ GL11.glMatrixMode(GL11.GL_PROJECTION);
+ GL11.glLoadIdentity();
+ double aspect = 1;
+ double near = 1; // near should be chosen as far into the scene as possible
+ double far = 1000;
+ double fov = 1; // 1 gives you a 90° field of view. It's tan(fov_angle)/2.
+ GL11.glFrustum(-aspect * near * fov, aspect * near * fov, -fov, fov, near, far);
+
+ GL11.glCullFace(GL11.GL_BACK);
+
+ long last = 0;
+
+ while (!Display.isCloseRequested())
+ {
+ // Clear the screen and depth buffer
+ GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
+
+ GL11.glBegin(GL11.GL_TRIANGLES);
+
+ for (int i = 0; i < md.triangleFaceCount; ++i)
+ {
+ int vertexA = md.trianglePointsX[i];
+ int vertexB = md.trianglePointsY[i];
+ int vertexC = md.trianglePointsZ[i];
+
+ int vertexAx = md.vertexX[vertexA];
+ int vertexAy = md.vertexY[vertexA];
+ int vertexAz = md.vertexZ[vertexA];
+
+ int vertexBx = md.vertexX[vertexB];
+ int vertexBy = md.vertexY[vertexB];
+ int vertexBz = md.vertexZ[vertexB];
+
+ int vertexCx = md.vertexX[vertexC];
+ int vertexCy = md.vertexY[vertexC];
+ int vertexCz = md.vertexZ[vertexC];
+
+ short hsb = md.faceColor[i];
+
+ int rgb = RS2HSB_to_RGB(hsb);
+ Color c = new Color(rgb);
+
+ // convert to range of 0-1
+ float rf = (float) c.getRed() / 255f;
+ float gf = (float) c.getGreen() / 255f;
+ float bf = (float) c.getBlue() / 255f;
+
+ GL11.glColor3f(rf, gf, bf);
+
+ GL11.glVertex3i(vertexAx, vertexAy, vertexAz - 50);
+ GL11.glVertex3i(vertexBx, vertexBy, vertexBz - 50);
+ GL11.glVertex3i(vertexCx, vertexCy, vertexCz - 50);
+ }
+
+ GL11.glEnd();
+
+ Display.update();
+ Display.sync(50); // fps
+
+ long delta = System.currentTimeMillis() - last;
+ last = System.currentTimeMillis();
+
+ Camera.create();
+ Camera.acceptInput(delta);
+
+ Camera.apply();
+ }
+
+ Display.destroy();
+ }
+
+ // found these two functions here https://www.rune-server.org/runescape-development/rs2-client/tools/589900-rs2-hsb-color-picker.html
+ public static int RGB_to_RS2HSB(int red, int green, int blue) {
+ float[] HSB = Color.RGBtoHSB(red, green, blue, null);
+ float hue = (HSB[0]);
+ float saturation = (HSB[1]);
+ float brightness = (HSB[2]);
+ int encode_hue = (int) (hue * 63); //to 6-bits
+ int encode_saturation = (int) (saturation * 7); //to 3-bits
+ int encode_brightness = (int) (brightness * 127); //to 7-bits
+ return (encode_hue << 10) + (encode_saturation << 7) + (encode_brightness);
+ }
+
+ public static int RS2HSB_to_RGB(int RS2HSB) {
+ int decode_hue = (RS2HSB >> 10) & 0x3f;
+ int decode_saturation = (RS2HSB >> 7) & 0x07;
+ int decode_brightness = (RS2HSB & 0x7f);
+ return Color.HSBtoRGB((float)decode_hue/63, (float)decode_saturation/7, (float)decode_brightness/127);
+ }
+}
diff --git a/model-viewer/src/main/java/net/runelite/modelviewer/Vector3f.java b/model-viewer/src/main/java/net/runelite/modelviewer/Vector3f.java
new file mode 100644
index 0000000000..f8c67fac49
--- /dev/null
+++ b/model-viewer/src/main/java/net/runelite/modelviewer/Vector3f.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2016, Adam
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. All advertising materials mentioning features or use of this software
+ * must display the following acknowledgement:
+ * This product includes software developed by Adam
+ * 4. Neither the name of the Adam nor the
+ * names of its contributors may be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY Adam ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL Adam BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package net.runelite.modelviewer;
+
+public class Vector3f
+{
+ public float x, y, z;
+
+ public Vector3f(float x, float y, float z)
+ {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "Vector3f{" + "x=" + x + ", y=" + y + ", z=" + z + '}';
+ }
+}
diff --git a/pom.xml b/pom.xml
index 324c559dfe..3551ac75d6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -70,6 +70,7 @@
runescape-client
runelite-client
runelite-api
+ model-viewer