diff --git a/.idea/artifacts/Old_School_RuneScape_Cache_Tools_jar.xml b/.idea/artifacts/Old_School_RuneScape_Cache_Tools_jar.xml
index bc18058..7398137 100644
--- a/.idea/artifacts/Old_School_RuneScape_Cache_Tools_jar.xml
+++ b/.idea/artifacts/Old_School_RuneScape_Cache_Tools_jar.xml
@@ -3,19 +3,59 @@
$PROJECT_DIR$/out/artifacts/Old_School_RuneScape_Cache_Tools_jar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index edbf2f6..537af65 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,12 +1,11 @@
-
-
-
+
+
\ No newline at end of file
diff --git a/.idea/libraries/openjfx_javafx_fxml.xml b/.idea/libraries/openjfx_javafx_fxml.xml
new file mode 100644
index 0000000..006ca87
--- /dev/null
+++ b/.idea/libraries/openjfx_javafx_fxml.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/openjfx_javafx_graphics.xml b/.idea/libraries/openjfx_javafx_graphics.xml
new file mode 100644
index 0000000..88109fe
--- /dev/null
+++ b/.idea/libraries/openjfx_javafx_graphics.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/openjfx_javafx_swing.xml b/.idea/libraries/openjfx_javafx_swing.xml
new file mode 100644
index 0000000..de8a631
--- /dev/null
+++ b/.idea/libraries/openjfx_javafx_swing.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index b6a274c..e634d84 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/Old-School-RuneScape-Cache-Tools.iml b/Old-School-RuneScape-Cache-Tools.iml
index 9111978..1afd0c8 100644
--- a/Old-School-RuneScape-Cache-Tools.iml
+++ b/Old-School-RuneScape-Cache-Tools.iml
@@ -15,5 +15,8 @@
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index f1c55b7..a92b5e1 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,8 @@
-# Old School RuneScape Cache Tools
+# Old School RuneScape Cache Tools (v0.5-beta)
### The ultimate suite of RuneScape Cache modification tools!
-Made with IntelliJ IDEA using Java 1.8 SDK.
+Made with IntelliJ IDEA using Java 1.8 SDK with various libraries including JavaFX.
## This suite features:
+### A simple 3D Model Viewer, built into the main window!
### Cache Functions
* Select Cache Data - A quick way to select a specific archive file in the cache.
* Search Cache (Work in progress)
@@ -67,11 +68,14 @@ Made with IntelliJ IDEA using Java 1.8 SDK.
* Gagravarr (https://github.com/Gagravarr): vorbis-java libraries.
* FormDev Software: for the FlatLaf interface look and feel.
* RuneLite Contributors (https://github.com/runelite/runelite): for the open source Old School RuneScape Client.
+* Suic (https://github.com/Suicolen): For the simple JFX model viewer/editor tool.
* Jagex: for being an inspiration in making this happen.
### Known Bugs/Issues
* Cache Explorer: Not able to dump/export all files of single archive - only the first file is exported?
* Cache Explorer: Some caches may not load correctly
* Data Decoders: SoundFont conversion - may not be perfect
+* Model Viewer: Does not work on RS2 yet
+* Model Viewer: May not work on Mac computers, a workaround would be running the project directly through IntelliJ IDEA.
## That's all for now. Enjoy!
\ No newline at end of file
diff --git a/lib/javafx-base-18-win.jar b/lib/javafx-base-18-win.jar
new file mode 100644
index 0000000..74454f5
Binary files /dev/null and b/lib/javafx-base-18-win.jar differ
diff --git a/lib/javafx-base-18.jar b/lib/javafx-base-18.jar
new file mode 100644
index 0000000..944b7c2
Binary files /dev/null and b/lib/javafx-base-18.jar differ
diff --git a/lib/javafx-base-19-ea+8-win.jar b/lib/javafx-base-19-ea+8-win.jar
new file mode 100644
index 0000000..2264277
Binary files /dev/null and b/lib/javafx-base-19-ea+8-win.jar differ
diff --git a/lib/javafx-base-19-ea+8.jar b/lib/javafx-base-19-ea+8.jar
new file mode 100644
index 0000000..51a701c
Binary files /dev/null and b/lib/javafx-base-19-ea+8.jar differ
diff --git a/lib/javafx-base-20-ea+2-win.jar b/lib/javafx-base-20-ea+2-win.jar
new file mode 100644
index 0000000..cc3c404
Binary files /dev/null and b/lib/javafx-base-20-ea+2-win.jar differ
diff --git a/lib/javafx-base-20-ea+2.jar b/lib/javafx-base-20-ea+2.jar
new file mode 100644
index 0000000..75dbbfa
Binary files /dev/null and b/lib/javafx-base-20-ea+2.jar differ
diff --git a/lib/javafx-base-20-ea+9-win.jar b/lib/javafx-base-20-ea+9-win.jar
new file mode 100644
index 0000000..4869dff
Binary files /dev/null and b/lib/javafx-base-20-ea+9-win.jar differ
diff --git a/lib/javafx-base-20-ea+9.jar b/lib/javafx-base-20-ea+9.jar
new file mode 100644
index 0000000..9a8f51f
Binary files /dev/null and b/lib/javafx-base-20-ea+9.jar differ
diff --git a/lib/javafx-controls-19-ea+8-win.jar b/lib/javafx-controls-19-ea+8-win.jar
new file mode 100644
index 0000000..f7c861e
Binary files /dev/null and b/lib/javafx-controls-19-ea+8-win.jar differ
diff --git a/lib/javafx-controls-19-ea+8.jar b/lib/javafx-controls-19-ea+8.jar
new file mode 100644
index 0000000..ace25a7
Binary files /dev/null and b/lib/javafx-controls-19-ea+8.jar differ
diff --git a/lib/javafx-fxml-19-ea+8-win.jar b/lib/javafx-fxml-19-ea+8-win.jar
new file mode 100644
index 0000000..e536a43
Binary files /dev/null and b/lib/javafx-fxml-19-ea+8-win.jar differ
diff --git a/lib/javafx-fxml-19-ea+8.jar b/lib/javafx-fxml-19-ea+8.jar
new file mode 100644
index 0000000..eeea8b0
Binary files /dev/null and b/lib/javafx-fxml-19-ea+8.jar differ
diff --git a/lib/javafx-graphics-18-win.jar b/lib/javafx-graphics-18-win.jar
new file mode 100644
index 0000000..53e3dd0
Binary files /dev/null and b/lib/javafx-graphics-18-win.jar differ
diff --git a/lib/javafx-graphics-18.jar b/lib/javafx-graphics-18.jar
new file mode 100644
index 0000000..54beda6
Binary files /dev/null and b/lib/javafx-graphics-18.jar differ
diff --git a/lib/javafx-graphics-19-ea+8-win.jar b/lib/javafx-graphics-19-ea+8-win.jar
new file mode 100644
index 0000000..c413ebc
Binary files /dev/null and b/lib/javafx-graphics-19-ea+8-win.jar differ
diff --git a/lib/javafx-graphics-19-ea+8.jar b/lib/javafx-graphics-19-ea+8.jar
new file mode 100644
index 0000000..5319c9c
Binary files /dev/null and b/lib/javafx-graphics-19-ea+8.jar differ
diff --git a/lib/javafx-graphics-20-ea+2-win.jar b/lib/javafx-graphics-20-ea+2-win.jar
new file mode 100644
index 0000000..fa5ca31
Binary files /dev/null and b/lib/javafx-graphics-20-ea+2-win.jar differ
diff --git a/lib/javafx-graphics-20-ea+2.jar b/lib/javafx-graphics-20-ea+2.jar
new file mode 100644
index 0000000..3443d5b
Binary files /dev/null and b/lib/javafx-graphics-20-ea+2.jar differ
diff --git a/lib/javafx-graphics-20-ea+9-win.jar b/lib/javafx-graphics-20-ea+9-win.jar
new file mode 100644
index 0000000..6d78b38
Binary files /dev/null and b/lib/javafx-graphics-20-ea+9-win.jar differ
diff --git a/lib/javafx-graphics-20-ea+9.jar b/lib/javafx-graphics-20-ea+9.jar
new file mode 100644
index 0000000..9aa4547
Binary files /dev/null and b/lib/javafx-graphics-20-ea+9.jar differ
diff --git a/lib/javafx-swing-18-win.jar b/lib/javafx-swing-18-win.jar
new file mode 100644
index 0000000..a5df726
Binary files /dev/null and b/lib/javafx-swing-18-win.jar differ
diff --git a/lib/javafx-swing-18.jar b/lib/javafx-swing-18.jar
new file mode 100644
index 0000000..1b7d43d
Binary files /dev/null and b/lib/javafx-swing-18.jar differ
diff --git a/lib/javafx-swing-20-ea+9-win.jar b/lib/javafx-swing-20-ea+9-win.jar
new file mode 100644
index 0000000..2ad1df7
Binary files /dev/null and b/lib/javafx-swing-20-ea+9-win.jar differ
diff --git a/lib/javafx-swing-20-ea+9.jar b/lib/javafx-swing-20-ea+9.jar
new file mode 100644
index 0000000..157411e
Binary files /dev/null and b/lib/javafx-swing-20-ea+9.jar differ
diff --git a/lib/netty-buffer-4.1.77.Final.jar b/lib/netty-buffer-4.1.77.Final.jar
new file mode 100644
index 0000000..815c4ba
Binary files /dev/null and b/lib/netty-buffer-4.1.77.Final.jar differ
diff --git a/lib/netty-common-4.1.77.Final.jar b/lib/netty-common-4.1.77.Final.jar
new file mode 100644
index 0000000..d4761d8
Binary files /dev/null and b/lib/netty-common-4.1.77.Final.jar differ
diff --git a/lib/streamex-0.8.1.jar b/lib/streamex-0.8.1.jar
new file mode 100644
index 0000000..e063f64
Binary files /dev/null and b/lib/streamex-0.8.1.jar differ
diff --git a/out/production/Old-School-RuneScape-Cache-Tools/net/runelite/cache/definitions/TextureDefinition.class b/out/production/Old-School-RuneScape-Cache-Tools/net/runelite/cache/definitions/TextureDefinition.class
index e6e0e43..74d3979 100644
Binary files a/out/production/Old-School-RuneScape-Cache-Tools/net/runelite/cache/definitions/TextureDefinition.class and b/out/production/Old-School-RuneScape-Cache-Tools/net/runelite/cache/definitions/TextureDefinition.class differ
diff --git a/src/java/com/application/GUI.java b/src/java/com/application/GUI.java
index b77ef19..45412b3 100644
--- a/src/java/com/application/GUI.java
+++ b/src/java/com/application/GUI.java
@@ -7,6 +7,11 @@
import com.sun.media.sound.SF2Soundbank;
import decoders.*;
import encoders.*;
+import javafx.application.Platform;
+import javafx.embed.swing.JFXPanel;
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+import modelviewer.ModelViewer;
import osrs.*;
import javax.sound.midi.MidiDevice;
@@ -60,11 +65,11 @@ public class GUI extends JFrame {
DefaultMutableTreeNode fileNode;
- public int selectedIndex;
+ public static int selectedIndex;
public int[] selectedIndices;
- public int selectedArchive;
+ public static int selectedArchive;
public int[] selectedArchives;
- public int selectedFile;
+ public static int selectedFile;
public int[] selectedFiles;
private static final File defaultCachePath;
@@ -73,9 +78,12 @@ public class GUI extends JFrame {
defaultCachePath = new File(System.getProperty("user.home") + File.separator + "jagexcache" + File.separator + "oldschool" + File.separator + "LIVE");
}
+ JFXPanel modelViewPanel;
+ Scene modelScene;
+ ModelViewer modelViewer;
public GUI() {
- super("Old School RuneScape Cache Tools v0.4-beta");
+ super("Old School RuneScape Cache Tools v0.5-beta");
setSize(640, 480);
setMinimumSize(new Dimension(640, 480));
setExtendedState(JFrame.MAXIMIZED_BOTH);
@@ -184,7 +192,7 @@ public GUI() {
JMenuItem test = new JMenuItem("Test tool");
test.addActionListener(e -> testTool());
- //toolsMenu.add(test);
+ toolsMenu.add(test);
JMenuItem convertToOldModel = new JMenuItem("Model - Convert to Old Format");
convertToOldModel.addActionListener(e -> new ModelOldConverter(this));
@@ -283,6 +291,7 @@ private void searchCacheData() {
}
private void testTool() {
+ /*
NPCComposition npcComposition = new NPCComposition();
npcComposition.decode(new Buffer(cacheLibrary.data(selectedIndex, selectedArchive, selectedFile)));
System.out.println("Name: " + npcComposition.name);
@@ -294,6 +303,7 @@ private void testTool() {
System.out.println("Walk Left Sequence: " + npcComposition.walkLeftSequence);
System.out.println("Walk Right Sequence: " + npcComposition.walkRightSequence);
System.out.println("Actions: " + Arrays.toString(npcComposition.actions));
+ */
}
private void editSynthPatch() {
@@ -801,6 +811,7 @@ private void chooseCacheFolder() {
}
private void loadCache(File cache) {
+
if (cache.isDirectory() && cache.exists()) {
try {
cacheLibrary = new CacheLibrary(cache.getPath(), false, null);
@@ -818,11 +829,26 @@ private void loadCache(File cache) {
AppConstants.cacheType = "RuneScape 3";
}
initModelToolModes();
+
+ modelViewer = new ModelViewer();
+ modelViewPanel = new JFXPanel();
+ Platform.runLater(() -> {
+ try {
+ modelScene = modelViewer.start(new Stage()).getScene();
+ modelViewPanel.setScene(modelScene);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ });
+
initFileViewer();
+ contentPreviewPane.revalidate();
} catch (Exception e) {
e.printStackTrace();
}
}
+
+
this.revalidate();
}
@@ -882,35 +908,51 @@ private void initFileViewer() {
cacheScrollPane = new JScrollPane(cacheTree);
cacheScrollPane.setViewportView(cacheTree);
+ cacheScrollPane.getVerticalScrollBar().setPreferredSize(new Dimension(20, 0));
JSplitPane cacheInfoSplitPanel = new JSplitPane();
JPanel cacheInfoPanel = new JPanel();
cacheInfoPanel.setLayout(new GridLayout());
+ SpringLayout springLayout = new SpringLayout();
JPanel cacheOperationsButtonPanel = new JPanel();
- cacheOperationsButtonPanel.setLayout(new FlowLayout());
+ cacheOperationsButtonPanel.setLayout(springLayout);
JButton addFilesButton = new JButton("Add Files");
addFilesButton.addActionListener(e -> addCacheFiles());
+ springLayout.putConstraint(SpringLayout.NORTH, addFilesButton, 0, SpringLayout.NORTH, cacheOperationsButtonPanel);
+ springLayout.putConstraint(SpringLayout.WEST, addFilesButton, 0, SpringLayout.WEST, cacheOperationsButtonPanel);
JButton exportFilesButton = new JButton("Export Files");
exportFilesButton.addActionListener(e -> exportFilesData());
+ springLayout.putConstraint(SpringLayout.NORTH, exportFilesButton, 0, SpringLayout.NORTH, cacheOperationsButtonPanel);
+ springLayout.putConstraint(SpringLayout.WEST, exportFilesButton, 100, SpringLayout.WEST, cacheOperationsButtonPanel);
JButton removeArchiveButton = new JButton("Remove Archive");
removeArchiveButton.addActionListener(e -> removeArchiveFile());
+ springLayout.putConstraint(SpringLayout.NORTH, removeArchiveButton, 0, SpringLayout.NORTH, cacheOperationsButtonPanel);
+ springLayout.putConstraint(SpringLayout.WEST, removeArchiveButton, 200, SpringLayout.WEST, cacheOperationsButtonPanel);
JButton removeFileButton = new JButton("Remove File");
removeFileButton.addActionListener(e -> removeCacheFile());
+ springLayout.putConstraint(SpringLayout.NORTH, removeFileButton, 30, SpringLayout.NORTH, cacheOperationsButtonPanel);
+ springLayout.putConstraint(SpringLayout.WEST, removeFileButton, 0, SpringLayout.WEST, cacheOperationsButtonPanel);
JButton setArchiveNameHashButton = new JButton("Set Archive name hash");
setArchiveNameHashButton.addActionListener(e -> setCacheArchiveNameHash());
+ springLayout.putConstraint(SpringLayout.NORTH, setArchiveNameHashButton, 30, SpringLayout.NORTH, cacheOperationsButtonPanel);
+ springLayout.putConstraint(SpringLayout.WEST, setArchiveNameHashButton, 150, SpringLayout.WEST, cacheOperationsButtonPanel);
JButton setArchiveNameButton = new JButton("Set Archive name");
setArchiveNameButton.addActionListener(e -> setCacheArchiveName());
+ springLayout.putConstraint(SpringLayout.NORTH, setArchiveNameButton, 60, SpringLayout.NORTH, cacheOperationsButtonPanel);
+ springLayout.putConstraint(SpringLayout.WEST, setArchiveNameButton, 0, SpringLayout.WEST, cacheOperationsButtonPanel);
JButton exportAllDataButton = new JButton("Export all Index data");
exportAllDataButton.addActionListener(e -> dumpAllDataFolders());
+ springLayout.putConstraint(SpringLayout.NORTH, exportAllDataButton, 60, SpringLayout.NORTH, cacheOperationsButtonPanel);
+ springLayout.putConstraint(SpringLayout.WEST, exportAllDataButton, 150, SpringLayout.WEST, cacheOperationsButtonPanel);
cacheOperationsButtonPanel.add(addFilesButton);
cacheOperationsButtonPanel.add(exportFilesButton);
@@ -1120,6 +1162,19 @@ private void initFileViewer() {
infoTable.revalidate();
}
}
+
+ if (modelViewer != null) {
+ modelViewer = null;
+ Platform.runLater(() -> {
+ try {
+ modelViewer = new ModelViewer();
+ modelScene = modelViewer.start(new Stage()).getScene();
+ modelViewPanel.setScene(modelScene);
+ } catch (Exception exception) {
+ exception.printStackTrace();
+ }
+ });
+ }
});
cacheInfoPanel.add(infoTable);
@@ -1127,24 +1182,21 @@ private void initFileViewer() {
cacheInfoSplitPanel.setBottomComponent(cacheOperationsButtonPanel);
cacheInfoSplitPanel.setTopComponent(cacheInfoPanel);
- cacheInfoSplitPanel.setEnabled(false);
cacheInfoSplitPanel.setResizeWeight(0.5);
cacheInfoSplitPanel.revalidate();
splitCacheViewPane.setLeftComponent(cacheScrollPane);
splitCacheViewPane.setRightComponent(cacheInfoSplitPanel);
- splitCacheViewPane.setEnabled(false);
splitCacheViewPane.setResizeWeight(0.5);
splitCacheViewPane.revalidate();
- //contentPreviewPane.add(new ModelViewer(new JFXPanel()));
+ contentPreviewPane.add(modelViewPanel);
contentPreviewPane.revalidate();
JSplitPane splitCacheDetailedViewPane = new JSplitPane();
splitCacheDetailedViewPane.setLeftComponent(splitCacheViewPane);
splitCacheDetailedViewPane.setRightComponent(contentPreviewPane);
- splitCacheDetailedViewPane.setEnabled(false);
- splitCacheDetailedViewPane.setResizeWeight(0.5);
+ splitCacheDetailedViewPane.setResizeWeight(0.3);
splitCacheDetailedViewPane.revalidate();
contentPanel.add(splitCacheDetailedViewPane, BorderLayout.CENTER);
diff --git a/src/java/com/application/Main.java b/src/java/com/application/Main.java
index ee36e82..f53bb22 100644
--- a/src/java/com/application/Main.java
+++ b/src/java/com/application/Main.java
@@ -1,11 +1,11 @@
package com.application;
-import com.formdev.flatlaf.FlatIntelliJLaf;
+import com.formdev.flatlaf.FlatDarculaLaf;
public class Main {
public static void main(String[] args) {
- FlatIntelliJLaf.setup();
+ FlatDarculaLaf.setup();
GUI gui = new GUI();
gui.setVisible(true);
}
diff --git a/src/java/modelviewer/ModelViewer.java b/src/java/modelviewer/ModelViewer.java
new file mode 100644
index 0000000..0159341
--- /dev/null
+++ b/src/java/modelviewer/ModelViewer.java
@@ -0,0 +1,18 @@
+package modelviewer;
+
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+
+import java.util.Objects;
+
+public class ModelViewer {
+
+ public Stage start(Stage primaryStage) throws Exception {
+ FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/ui.fxml"));
+ Scene scene = new Scene(fxmlLoader.load());
+ scene.getStylesheets().add(Objects.requireNonNull(getClass().getResource("/css/darktheme.css")).toExternalForm());
+ primaryStage.setScene(scene);
+ return primaryStage;
+ }
+}
diff --git a/src/java/modelviewer/controllers/ModelController.java b/src/java/modelviewer/controllers/ModelController.java
new file mode 100644
index 0000000..74e2e5e
--- /dev/null
+++ b/src/java/modelviewer/controllers/ModelController.java
@@ -0,0 +1,135 @@
+package modelviewer.controllers;
+
+import com.application.AppConstants;
+import com.application.GUI;
+import com.displee.cache.CacheLibrary;
+import javafx.fxml.FXML;
+import javafx.fxml.Initializable;
+import javafx.scene.AmbientLight;
+import javafx.scene.Group;
+import javafx.scene.SceneAntialiasing;
+import javafx.scene.SubScene;
+import javafx.scene.layout.AnchorPane;
+import javafx.scene.paint.Color;
+import modelviewer.model.Grid3D;
+import modelviewer.scene.RSMeshGroup;
+import modelviewer.scene.camera.OrbitCamera;
+import net.runelite.cache.definitions.ModelDefinition;
+import net.runelite.cache.definitions.loaders.ModelLoader;
+import rs3.RS3ModelData;
+import rshd.ModelData;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Objects;
+import java.util.ResourceBundle;
+
+public class ModelController implements Initializable {
+
+ @FXML
+ private AnchorPane modelPane;
+
+ private RSMeshGroup meshGroup;
+ private Group scene;
+
+ @Override
+ public void initialize(URL location, ResourceBundle resources) {
+ updateModel(GUI.cacheLibrary, GUI.selectedIndex, GUI.selectedArchive, GUI.selectedFile);
+ }
+
+ public void initScene(ModelDefinition model) throws IOException {
+ meshGroup = new RSMeshGroup(model);
+ meshGroup.buildMeshes();
+ scene = buildScene();
+ Group grid = new Grid3D().create(48f, 1.25f);
+ scene.getChildren().add(grid);
+ SubScene subScene = create3DScene();
+ scene.getChildren().add(new AmbientLight(Color.WHITE));
+ modelPane.getChildren().addAll(subScene);
+ }
+
+ private Group buildScene() {
+ Group group = new Group();
+ group.getChildren().addAll(meshGroup.getMeshes());
+ return group;
+ }
+
+ private SubScene create3DScene() {
+ SubScene scene3D = new SubScene(scene, modelPane.getPrefWidth(), modelPane.getPrefHeight(), true, SceneAntialiasing.BALANCED);
+ scene3D.setFill(Color.rgb(30, 30, 30));
+ new OrbitCamera(scene3D, scene);
+ return scene3D;
+ }
+
+ public void updateModel(CacheLibrary cacheLibrary, int selectedIndex, int selectedArchive, int selectedFile) {
+
+ if (AppConstants.cacheType.equals("RuneScape 2")) {
+ ModelLoader modelLoader = new ModelLoader();
+ ModelDefinition model;
+ if (selectedIndex == 7) {
+ model = modelLoader.load(0, Objects.requireNonNull(cacheLibrary.data(selectedIndex, selectedArchive, selectedFile)));
+ } else {
+ model = modelLoader.load(0, Objects.requireNonNull(cacheLibrary.data(7, 1, 0)));
+ }
+ if (model != null) {
+ try {
+ initScene(model);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ if (AppConstants.cacheType.equals("Old School RuneScape")) {
+ ModelLoader modelLoader = new ModelLoader();
+ ModelDefinition model;
+ if (selectedIndex == 7) {
+ model = modelLoader.load(0, Objects.requireNonNull(cacheLibrary.data(selectedIndex, selectedArchive, selectedFile)));
+ } else {
+ model = modelLoader.load(0, Objects.requireNonNull(cacheLibrary.data(7, 1, 0)));
+ }
+ if (model != null) {
+ try {
+ initScene(model);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ if (AppConstants.cacheType.equals("RuneScape High Definition")) {
+ ModelData modelData = new ModelData();
+ ModelDefinition model;
+ if (selectedIndex == 7) {
+ model = modelData.load(0, Objects.requireNonNull(cacheLibrary.data(selectedIndex, selectedArchive, selectedFile)));
+ } else {
+ model = modelData.load(0, Objects.requireNonNull(cacheLibrary.data(7, 1, 0)));
+ }
+ if (model != null) {
+ try {
+ initScene(model);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ if (AppConstants.cacheType.equals("RuneScape 3")) {
+ RS3ModelData modelData = new RS3ModelData(0);
+ ModelDefinition model;
+ if (selectedIndex == 7) {
+ model = modelData.load(0, Objects.requireNonNull(cacheLibrary.data(selectedIndex, selectedArchive, selectedFile)), cacheLibrary);
+ } else {
+ model = modelData.load(0, Objects.requireNonNull(cacheLibrary.data(7, 1, 0)), cacheLibrary);
+ }
+ if (model != null) {
+ try {
+ initScene(model);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+ }
+
diff --git a/src/java/modelviewer/model/Grid3D.java b/src/java/modelviewer/model/Grid3D.java
new file mode 100644
index 0000000..d6e9bc2
--- /dev/null
+++ b/src/java/modelviewer/model/Grid3D.java
@@ -0,0 +1,101 @@
+package modelviewer.model;
+
+import javafx.scene.Group;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.PhongMaterial;
+import javafx.scene.shape.CullFace;
+import javafx.scene.shape.DrawMode;
+import javafx.scene.transform.Rotate;
+import javafx.scene.transform.Translate;
+import modelviewer.polygonalmesh.PolygonMesh;
+import modelviewer.polygonalmesh.PolygonMeshView;
+
+public class Grid3D {
+
+ public Group create(float size, float delta) {
+ if (delta < 0.01f) {
+ delta = 0.01f;
+ }
+
+ final PolygonMesh plane2 = createQuadrilateralMesh(size, size, (int) (size / delta), (int) (size / delta));
+
+ PolygonMeshView meshViewXZ2 = new PolygonMeshView(plane2);
+ meshViewXZ2.setDrawMode(DrawMode.LINE);
+ meshViewXZ2.setCullFace(CullFace.NONE);
+ PhongMaterial mat = new PhongMaterial();
+ mat.setDiffuseColor(Color.BLACK);
+ meshViewXZ2.setMaterial(mat);
+ meshViewXZ2.getTransforms().add(new Translate(size / 1000f, size / 1000f, 0));
+ meshViewXZ2.getTransforms().add(new Rotate(90, Rotate.X_AXIS));
+
+ return new Group(meshViewXZ2);
+ }
+
+ private PolygonMesh createQuadrilateralMesh(float width, float height, int subDivX, int subDivY) {
+ final float minX = -width / 2f;
+ final float minY = -height / 2f;
+ final float maxX = width / 2f;
+ final float maxY = height / 2f;
+
+ final int pointSize = 3;
+ final int texCoordSize = 2;
+ // 4 point indices and 4 texCoord indices per face
+ final int faceSize = 8;
+ int numDivX = subDivX + 1;
+ int numVerts = (subDivY + 1) * numDivX;
+ float[] points = new float[numVerts * pointSize];
+ float[] texCoords = new float[numVerts * texCoordSize];
+ int faceCount = subDivX * subDivY;
+ int[][] faces = new int[faceCount][faceSize];
+
+ // Create points and texCoords
+ for (int y = 0; y <= subDivY; y++) {
+ float dy = (float) y / subDivY;
+ double fy = (1 - dy) * minY + dy * maxY;
+
+ for (int x = 0; x <= subDivX; x++) {
+ float dx = (float) x / subDivX;
+ double fx = (1 - dx) * minX + dx * maxX;
+
+ int index = y * numDivX * pointSize + (x * pointSize);
+ points[index] = (float) fx;
+ points[index + 1] = (float) fy;
+ points[index + 2] = 0.0f;
+
+ index = y * numDivX * texCoordSize + (x * texCoordSize);
+ texCoords[index] = dx;
+ texCoords[index + 1] = dy;
+ }
+ }
+
+ // Create faces
+ int index = 0;
+ for (int y = 0; y < subDivY; y++) {
+ for (int x = 0; x < subDivX; x++) {
+ int p00 = y * numDivX + x;
+ int p01 = p00 + 1;
+ int p10 = p00 + numDivX;
+ int p11 = p10 + 1;
+ int tc00 = y * numDivX + x;
+ int tc01 = tc00 + 1;
+ int tc10 = tc00 + numDivX;
+ int tc11 = tc10 + 1;
+
+ faces[index][0] = p00;
+ faces[index][1] = tc00;
+ faces[index][2] = p10;
+ faces[index][3] = tc10;
+ faces[index][4] = p11;
+ faces[index][5] = tc11;
+ faces[index][6] = p01;
+ faces[index++][7] = tc01;
+ }
+ }
+
+ int[] smooth = new int[faceCount];
+
+ PolygonMesh mesh = new PolygonMesh(points, texCoords, faces);
+ mesh.getFaceSmoothingGroups().addAll(smooth);
+ return mesh;
+ }
+}
diff --git a/src/java/modelviewer/model/Vector3i.java b/src/java/modelviewer/model/Vector3i.java
new file mode 100644
index 0000000..8c6c0b7
--- /dev/null
+++ b/src/java/modelviewer/model/Vector3i.java
@@ -0,0 +1,51 @@
+package modelviewer.model;
+
+import java.util.Objects;
+
+public final class Vector3i {
+
+ private final int x;
+ private final int y;
+ private final int z;
+
+ public Vector3i(int x, int y, int z) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ }
+
+ public int x() {
+ return x;
+ }
+
+ public int y() {
+ return y;
+ }
+
+ public int z() {
+ return z;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) return true;
+ if (obj == null || obj.getClass() != this.getClass()) return false;
+ Vector3i that = (Vector3i) obj;
+ return this.x == that.x &&
+ this.y == that.y &&
+ this.z == that.z;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(x, y, z);
+ }
+
+ @Override
+ public String toString() {
+ return "Vector3i[" +
+ "x=" + x + ", " +
+ "y=" + y + ", " +
+ "z=" + z + ']';
+ }
+}
\ No newline at end of file
diff --git a/src/java/modelviewer/model/Xform.java b/src/java/modelviewer/model/Xform.java
new file mode 100644
index 0000000..55cc645
--- /dev/null
+++ b/src/java/modelviewer/model/Xform.java
@@ -0,0 +1,26 @@
+package modelviewer.model;
+
+import javafx.scene.Group;
+import javafx.scene.transform.Rotate;
+import javafx.scene.transform.Scale;
+import javafx.scene.transform.Translate;
+import lombok.AccessLevel;
+import lombok.experimental.FieldDefaults;
+
+@FieldDefaults(level = AccessLevel.PUBLIC)
+public class Xform extends Group {
+
+ Translate t = new Translate();
+ Translate p = new Translate();
+ Rotate rx = new Rotate();
+ Rotate ry = new Rotate();
+ Rotate rz = new Rotate();
+ Scale s = new Scale();
+
+ public Xform() {
+ rx.setAxis(Rotate.X_AXIS);
+ ry.setAxis(Rotate.Y_AXIS);
+ rz.setAxis(Rotate.Z_AXIS);
+ getTransforms().addAll(t, rz, ry, rx, s);
+ }
+}
diff --git a/src/java/modelviewer/polygonalmesh/OriginalPointArray.java b/src/java/modelviewer/polygonalmesh/OriginalPointArray.java
new file mode 100644
index 0000000..d7c33d4
--- /dev/null
+++ b/src/java/modelviewer/polygonalmesh/OriginalPointArray.java
@@ -0,0 +1,47 @@
+package modelviewer.polygonalmesh;
+
+/*
+ * Copyright (c) 2010, 2014, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - 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.
+ * - Neither the name of Oracle Corporation 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.
+ */
+
+public class OriginalPointArray extends SymbolicPointArray {
+ PolygonMesh mesh;
+
+ public OriginalPointArray(PolygonMesh mesh) {
+ super(new float[mesh.getPoints().size()]);
+ this.mesh = mesh;
+ }
+
+ @Override
+ public void update() {
+ mesh.getPoints().copyTo(0, data, 0, data.length);
+ }
+}
\ No newline at end of file
diff --git a/src/java/modelviewer/polygonalmesh/PolygonMesh.java b/src/java/modelviewer/polygonalmesh/PolygonMesh.java
new file mode 100644
index 0000000..f3a31ba
--- /dev/null
+++ b/src/java/modelviewer/polygonalmesh/PolygonMesh.java
@@ -0,0 +1,99 @@
+package modelviewer.polygonalmesh;
+
+/*
+ * Copyright (c) 2010, 2014, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - 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.
+ * - Neither the name of Oracle Corporation 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.
+ */
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableFloatArray;
+import javafx.collections.ObservableIntegerArray;
+
+/**
+ * A Mesh where each face can be a Polygon
+ *
+ * can convert to using ObservableIntegerArray
+ */
+public class PolygonMesh {
+ private final ObservableFloatArray points = FXCollections.observableFloatArray();
+ private final ObservableFloatArray texCoords = FXCollections.observableFloatArray();
+ public int[][] faces = new int[0][0];
+ private final ObservableIntegerArray faceSmoothingGroups = FXCollections.observableIntegerArray();
+ protected int numEdgesInFaces = -1; // TODO invalidate automatically by listening to faces (whenever it is an observable)
+
+ public PolygonMesh() {
+ }
+
+ public PolygonMesh(float[] points, float[] texCoords, int[][] faces) {
+ this.points.addAll(points);
+ this.texCoords.addAll(texCoords);
+ this.faces = faces;
+ }
+
+ public ObservableFloatArray getPoints() {
+ return points;
+ }
+
+ public ObservableFloatArray getTexCoords() {
+ return texCoords;
+ }
+
+ public ObservableIntegerArray getFaceSmoothingGroups() {
+ return faceSmoothingGroups;
+ }
+
+ public int getNumEdgesInFaces() {
+ if (numEdgesInFaces == -1) {
+ numEdgesInFaces = 0;
+ for (int[] face : faces) {
+ numEdgesInFaces += face.length;
+ }
+ numEdgesInFaces /= 2;
+ }
+ return numEdgesInFaces;
+ }
+
+ // TODO: Hardcode to constants for FX 8 (only one vertex format)
+ private static final int NUM_COMPONENTS_PER_POINT = 3;
+ private static final int NUM_COMPONENTS_PER_TEXCOORD = 2;
+ private static final int NUM_COMPONENTS_PER_FACE = 6;
+
+ public int getPointElementSize() {
+ return NUM_COMPONENTS_PER_POINT;
+ }
+
+ public int getTexCoordElementSize() {
+ return NUM_COMPONENTS_PER_TEXCOORD;
+ }
+
+ public int getFaceElementSize() {
+ return NUM_COMPONENTS_PER_FACE;
+ }
+}
\ No newline at end of file
diff --git a/src/java/modelviewer/polygonalmesh/PolygonMeshView.java b/src/java/modelviewer/polygonalmesh/PolygonMeshView.java
new file mode 100644
index 0000000..3cad705
--- /dev/null
+++ b/src/java/modelviewer/polygonalmesh/PolygonMeshView.java
@@ -0,0 +1,478 @@
+package modelviewer.polygonalmesh;/*
+ * Copyright (c) 2010, 2014, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - 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.
+ * - Neither the name of Oracle Corporation 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.
+ */
+
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.ArrayChangeListener;
+import javafx.collections.ObservableFloatArray;
+import javafx.scene.Parent;
+import javafx.scene.paint.Material;
+import javafx.scene.shape.CullFace;
+import javafx.scene.shape.DrawMode;
+import javafx.scene.shape.MeshView;
+import javafx.scene.shape.TriangleMesh;
+
+import java.util.Arrays;
+
+/**
+ * A MeshView node for Polygon Meshes
+ */
+public class PolygonMeshView extends Parent {
+ private static final boolean DEBUG = false;
+ private final MeshView meshView = new MeshView();
+
+ private TriangleMesh triangleMesh = new TriangleMesh();
+
+ // this is null if no subdivision is happening (i.e. subdivisionLevel = 0);
+ private SubdivisionMesh subdivisionMesh;
+
+ private final ArrayChangeListener meshPointsListener = (t, bln, i, i1) -> {
+ pointsDirty = true;
+ updateMesh();
+ };
+ private final ArrayChangeListener meshTexCoordListener = (t, bln, i, i1) -> {
+ texCoordsDirty = true;
+ updateMesh();
+ };
+
+ private boolean pointsDirty = true;
+ private boolean pointsSizeDirty = true;
+ private boolean texCoordsDirty = true;
+ private boolean facesDirty = true;
+
+ // =========================================================================
+ // PROPERTIES
+
+ /**
+ * Specifies the 3D mesh data of this {@code MeshView}.
+ *
+ * @defaultValue null
+ */
+ private ObjectProperty meshProperty;
+
+ public PolygonMesh getMesh() {
+ return meshProperty().get();
+ }
+
+ public void setMesh(PolygonMesh mesh) {
+ meshProperty().set(mesh);
+ }
+
+ public ObjectProperty meshProperty() {
+ if (meshProperty == null) {
+ meshProperty = new SimpleObjectProperty();
+ meshProperty.addListener((observable, oldValue, newValue) -> {
+ if (oldValue != null) {
+ oldValue.getPoints().removeListener(meshPointsListener);
+ oldValue.getPoints().removeListener(meshTexCoordListener);
+ }
+
+ meshProperty.set(newValue);
+
+ pointsDirty = pointsSizeDirty = texCoordsDirty = facesDirty = true;
+ updateMesh();
+
+ if (newValue != null) {
+ newValue.getPoints().addListener(meshPointsListener);
+ newValue.getTexCoords().addListener(meshTexCoordListener);
+ }
+ });
+ }
+ return meshProperty;
+ }
+
+ /**
+ * Defines the drawMode this {@code Shape3D}.
+ *
+ * @defaultValue DrawMode.FILL
+ */
+ private ObjectProperty drawMode;
+
+ public final void setDrawMode(DrawMode value) {
+ drawModeProperty().set(value);
+ }
+
+ public final DrawMode getDrawMode() {
+ return drawMode == null ? DrawMode.FILL : drawMode.get();
+ }
+
+ public final ObjectProperty drawModeProperty() {
+ if (drawMode == null) {
+ drawMode = new SimpleObjectProperty(PolygonMeshView.this, "drawMode", DrawMode.FILL) {
+ @Override
+ protected void invalidated() {
+ meshView.setDrawMode(get());
+ pointsDirty = pointsSizeDirty = texCoordsDirty = facesDirty = true;
+ updateMesh();
+ }
+ };
+ }
+ return drawMode;
+ }
+
+ /**
+ * Defines the drawMode this {@code Shape3D}.
+ *
+ * @defaultValue CullFace.BACK
+ */
+ private ObjectProperty cullFace;
+
+ public final void setCullFace(CullFace value) {
+ cullFaceProperty().set(value);
+ }
+
+ public final CullFace getCullFace() {
+ return cullFace == null ? CullFace.BACK : cullFace.get();
+ }
+
+ public final ObjectProperty cullFaceProperty() {
+ if (cullFace == null) {
+ cullFace = new SimpleObjectProperty(PolygonMeshView.this, "cullFace", CullFace.BACK) {
+ @Override
+ protected void invalidated() {
+ meshView.setCullFace(get());
+ }
+ };
+ }
+ return cullFace;
+ }
+
+ /**
+ * Defines the material this {@code Shape3D}.
+ * The default material is null. If {@code Material} is null, a PhongMaterial
+ * with a diffuse color of Color.LIGHTGRAY is used for rendering.
+ *
+ * @defaultValue null
+ */
+ private ObjectProperty materialProperty = new SimpleObjectProperty();
+
+ public Material getMaterial() {
+ return materialProperty.get();
+ }
+
+ public void setMaterial(Material material) {
+ materialProperty.set(material);
+ }
+
+ public ObjectProperty materialProperty() {
+ return materialProperty;
+ }
+
+ /**
+ * Number of iterations of Catmull Clark subdivision to apply to the mesh
+ *
+ * @defaultValue 0
+ */
+ private SimpleIntegerProperty subdivisionLevelProperty;
+
+ public void setSubdivisionLevel(int subdivisionLevel) {
+ subdivisionLevelProperty().set(subdivisionLevel);
+ }
+
+ public int getSubdivisionLevel() {
+ return subdivisionLevelProperty == null ? 0 : subdivisionLevelProperty.get();
+ }
+
+ public SimpleIntegerProperty subdivisionLevelProperty() {
+ if (subdivisionLevelProperty == null) {
+ subdivisionLevelProperty = new SimpleIntegerProperty(getSubdivisionLevel()) {
+ @Override
+ protected void invalidated() {
+ // create SubdivisionMesh if subdivisionLevel is greater than 0
+ if ((getSubdivisionLevel() > 0) && (subdivisionMesh == null)) {
+ subdivisionMesh = new SubdivisionMesh(getMesh(), getSubdivisionLevel(), getBoundaryMode(), getMapBorderMode());
+ subdivisionMesh.getOriginalMesh()
+ .getPoints()
+ .addListener((t, bln, i, i1) -> subdivisionMesh.update());
+ setMesh(subdivisionMesh);
+ }
+ if (subdivisionMesh != null) {
+ subdivisionMesh.setSubdivisionLevel(getSubdivisionLevel());
+ subdivisionMesh.update();
+ }
+ pointsDirty = pointsSizeDirty = texCoordsDirty = facesDirty = true;
+ updateMesh();
+ }
+ };
+ }
+ return subdivisionLevelProperty;
+ }
+
+ /**
+ * Texture mapping boundary rule for Catmull Clark subdivision applied to the mesh
+ *
+ * @defaultValue BoundaryMode.CREASE_EDGES
+ */
+ private SimpleObjectProperty boundaryMode;
+
+ public void setBoundaryMode(SubdivisionMesh.BoundaryMode boundaryMode) {
+ boundaryModeProperty().set(boundaryMode);
+ }
+
+ public SubdivisionMesh.BoundaryMode getBoundaryMode() {
+ return boundaryMode == null ? SubdivisionMesh.BoundaryMode.CREASE_EDGES : boundaryMode.get();
+ }
+
+ public SimpleObjectProperty boundaryModeProperty() {
+ if (boundaryMode == null) {
+ boundaryMode = new SimpleObjectProperty(getBoundaryMode()) {
+ @Override
+ protected void invalidated() {
+ if (subdivisionMesh != null) {
+ subdivisionMesh.setBoundaryMode(getBoundaryMode());
+ subdivisionMesh.update();
+ }
+ pointsDirty = true;
+ updateMesh();
+ }
+ };
+ }
+ return boundaryMode;
+ }
+
+ /**
+ * Texture mapping smoothness option for Catmull Clark subdivision applied to the mesh
+ *
+ * @defaultValue MapBorderMode.NOT_SMOOTH
+ */
+ private SimpleObjectProperty mapBorderMode;
+
+ public void setMapBorderMode(SubdivisionMesh.MapBorderMode mapBorderMode) {
+ mapBorderModeProperty().set(mapBorderMode);
+ }
+
+ public SubdivisionMesh.MapBorderMode getMapBorderMode() {
+ return mapBorderMode == null ? SubdivisionMesh.MapBorderMode.NOT_SMOOTH : mapBorderMode.get();
+ }
+
+ public SimpleObjectProperty mapBorderModeProperty() {
+ if (mapBorderMode == null) {
+ mapBorderMode = new SimpleObjectProperty(getMapBorderMode()) {
+ @Override
+ protected void invalidated() {
+ if (subdivisionMesh != null) {
+ subdivisionMesh.setMapBorderMode(getMapBorderMode());
+ subdivisionMesh.update();
+ }
+ texCoordsDirty = true;
+ updateMesh();
+ }
+ };
+ }
+ return mapBorderMode;
+ }
+
+ // =========================================================================
+ // CONSTRUCTORS
+
+ public PolygonMeshView() {
+ meshView.materialProperty().bind(materialProperty());
+ getChildren().add(meshView);
+ }
+
+ public PolygonMeshView(PolygonMesh mesh) {
+ this();
+ setMesh(mesh);
+ }
+
+ // =========================================================================
+ // PRIVATE METHODS
+
+ private void updateMesh() {
+ PolygonMesh pmesh = getMesh();
+ if (pmesh == null || pmesh.faces == null) {
+ triangleMesh = new TriangleMesh();
+ meshView.setMesh(triangleMesh);
+ return;
+ }
+
+ final int pointElementSize = triangleMesh.getPointElementSize();
+ final int faceElementSize = triangleMesh.getFaceElementSize();
+ final boolean isWireframe = getDrawMode() == DrawMode.LINE;
+ if (DEBUG) System.out.println("UPDATE MESH -- " + (isWireframe ? "WIREFRAME" : "SOLID"));
+ final int numOfPoints = pmesh.getPoints().size() / pointElementSize;
+ if (DEBUG) System.out.println("numOfPoints = " + numOfPoints);
+
+ if (isWireframe) {
+ // The current triangleMesh implementation gives buggy behavior when the size of faces are shrunken
+ // Create a new TriangleMesh as a work around
+ // [JIRA] (RT-31178)
+ if (texCoordsDirty || facesDirty || pointsSizeDirty) {
+ triangleMesh = new TriangleMesh();
+ pointsDirty = pointsSizeDirty = texCoordsDirty = facesDirty = true; // to fill in the new triangle mesh
+ }
+ if (facesDirty) {
+ facesDirty = false;
+ // create faces for each edge
+ int[] facesArray = new int[pmesh.getNumEdgesInFaces() * faceElementSize];
+ int facesInd = 0;
+ int pointsInd = pmesh.getPoints().size();
+ for (int[] face : pmesh.faces) {
+ if (DEBUG)
+ System.out.println("face.length = " + (face.length / 2) + " -- " + Arrays.toString(face));
+ int lastPointIndex = face[face.length - 2];
+ if (DEBUG) System.out.println(" lastPointIndex = " + lastPointIndex);
+ for (int p = 0; p < face.length; p += 2) {
+ int pointIndex = face[p];
+ if (DEBUG)
+ System.out.println(" connecting point[" + lastPointIndex + "] to point[" + pointIndex + "]");
+ facesArray[facesInd++] = lastPointIndex;
+ facesArray[facesInd++] = 0;
+ facesArray[facesInd++] = pointIndex;
+ facesArray[facesInd++] = 0;
+ facesArray[facesInd++] = pointsInd / pointElementSize;
+ facesArray[facesInd++] = 0;
+ if (DEBUG) System.out.println(" facesInd = " + facesInd);
+ pointsInd += pointElementSize;
+ lastPointIndex = pointIndex;
+ }
+ }
+ triangleMesh.getFaces().setAll(facesArray);
+ triangleMesh.getFaceSmoothingGroups().clear();
+ }
+ if (texCoordsDirty) {
+ texCoordsDirty = false;
+ // set simple texCoords for wireframe
+ triangleMesh.getTexCoords().setAll(0, 0);
+ }
+ if (pointsDirty) {
+ pointsDirty = false;
+ // create points and copy over points to the first part of the array
+ float[] pointsArray = new float[pmesh.getPoints()
+ .size() + pmesh.getNumEdgesInFaces() * 3];
+ pmesh.getPoints().copyTo(0, pointsArray, 0, pmesh.getPoints().size());
+
+ // add point for each edge
+ int pointsInd = pmesh.getPoints().size();
+ for (int[] face : pmesh.faces) {
+ int lastPointIndex = face[face.length - 2];
+ for (int p = 0; p < face.length; p += 2) {
+ int pointIndex = face[p];
+ // get start and end point
+ final float x1 = pointsArray[lastPointIndex * pointElementSize];
+ final float y1 = pointsArray[lastPointIndex * pointElementSize + 1];
+ final float z1 = pointsArray[lastPointIndex * pointElementSize + 2];
+ final float x2 = pointsArray[pointIndex * pointElementSize];
+ final float y2 = pointsArray[pointIndex * pointElementSize + 1];
+ final float z2 = pointsArray[pointIndex * pointElementSize + 2];
+ final float distance = Math.abs(distanceBetweenPoints(x1, y1, z1, x2, y2, z2));
+ final float offset = distance / 1000;
+ // add new point
+ pointsArray[pointsInd++] = x2 + offset;
+ pointsArray[pointsInd++] = y2 + offset;
+ pointsArray[pointsInd++] = z2 + offset;
+ lastPointIndex = pointIndex;
+ }
+ }
+ triangleMesh.getPoints().setAll(pointsArray);
+ }
+ } else {
+ // The current triangleMesh implementation gives buggy behavior when the size of faces are shrunken
+ // Create a new TriangleMesh as a work around
+ // [JIRA] (RT-31178)
+ if (texCoordsDirty || facesDirty || pointsSizeDirty) {
+ triangleMesh = new TriangleMesh();
+ pointsDirty = pointsSizeDirty = texCoordsDirty = facesDirty = true; // to fill in the new triangle mesh
+ }
+ if (facesDirty) {
+ facesDirty = false;
+ // create faces and break into triangles
+ final int numOfFacesBefore = pmesh.faces.length;
+ final int numOfFacesAfter = pmesh.getNumEdgesInFaces() - 2 * numOfFacesBefore;
+ int[] facesArray = new int[numOfFacesAfter * faceElementSize];
+ int[] smoothingGroupsArray = new int[numOfFacesAfter];
+ int facesInd = 0;
+ for (int f = 0; f < pmesh.faces.length; f++) {
+ int[] face = pmesh.faces[f];
+ int currentSmoothGroup = pmesh.getFaceSmoothingGroups().get(f);
+ if (DEBUG)
+ System.out.println("face.length = " + face.length + " -- " + Arrays.toString(face));
+ int firstPointIndex = face[0];
+ int firstTexIndex = face[1];
+ int lastPointIndex = face[2];
+ int lastTexIndex = face[3];
+ for (int p = 4; p < face.length; p += 2) {
+ int pointIndex = face[p];
+ int texIndex = face[p + 1];
+ facesArray[facesInd * faceElementSize] = firstPointIndex;
+ facesArray[facesInd * faceElementSize + 1] = firstTexIndex;
+ facesArray[facesInd * faceElementSize + 2] = lastPointIndex;
+ facesArray[facesInd * faceElementSize + 3] = lastTexIndex;
+ facesArray[facesInd * faceElementSize + 4] = pointIndex;
+ facesArray[facesInd * faceElementSize + 5] = texIndex;
+ smoothingGroupsArray[facesInd] = currentSmoothGroup;
+ facesInd++;
+ lastPointIndex = pointIndex;
+ lastTexIndex = texIndex;
+ }
+ }
+ triangleMesh.getFaces().setAll(facesArray);
+ triangleMesh.getFaceSmoothingGroups().setAll(smoothingGroupsArray);
+ }
+ if (texCoordsDirty) {
+ texCoordsDirty = false;
+ triangleMesh.getTexCoords().setAll(pmesh.getTexCoords());
+ }
+ if (pointsDirty) {
+ pointsDirty = false;
+ triangleMesh.getPoints().setAll(pmesh.getPoints());
+ }
+ }
+
+ if (DEBUG) System.out.println("CREATING TRIANGLE MESH");
+ if (DEBUG)
+ System.out.println(" points = " + Arrays.toString(((TriangleMesh) meshView.getMesh())
+ .getPoints()
+ .toArray(null)));
+ if (DEBUG)
+ System.out.println(" texCoords = " + Arrays.toString(((TriangleMesh) meshView.getMesh())
+ .getTexCoords()
+ .toArray(null)));
+ if (DEBUG)
+ System.out.println(" faces = " + Arrays.toString(((TriangleMesh) meshView.getMesh())
+ .getFaces()
+ .toArray(null)));
+
+ if (meshView.getMesh() != triangleMesh) {
+ meshView.setMesh(triangleMesh);
+ }
+ pointsDirty = pointsSizeDirty = texCoordsDirty = facesDirty = false;
+ }
+
+ private float distanceBetweenPoints(float x1, float y1, float z1, float x2, float y2, float z2) {
+ return (float) Math.sqrt(
+ Math.pow(z2 - z1, 2) +
+ Math.pow(x2 - x1, 2) +
+ Math.pow(y2 - y1, 2));
+ }
+}
\ No newline at end of file
diff --git a/src/java/modelviewer/polygonalmesh/SubdividedPointArray.java b/src/java/modelviewer/polygonalmesh/SubdividedPointArray.java
new file mode 100644
index 0000000..5ad79db
--- /dev/null
+++ b/src/java/modelviewer/polygonalmesh/SubdividedPointArray.java
@@ -0,0 +1,168 @@
+package modelviewer.polygonalmesh;
+
+/*
+ * Copyright (c) 2010, 2014, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - 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.
+ * - Neither the name of Oracle Corporation 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.
+ */
+
+import java.util.Arrays;
+
+
+public class SubdividedPointArray extends SymbolicPointArray {
+ private final float[] controlPoints; // points of the previous subdivision level
+ private final int[][] controlInds; // indices corresponding to controlPoints
+ private final float[][] controlFactors; // factors corresponding to controlPoints
+ private final int[][] inds;
+ private final float[][] factors;
+
+ private final SubdivisionMesh.BoundaryMode boundaryMode;
+
+ private int currPoint = 0;
+
+ public SubdividedPointArray(SymbolicPointArray controlPointArray, int numPoints, SubdivisionMesh.BoundaryMode boundaryMode) {
+ super(new float[NUM_COMPONENTS_PER_POINT * numPoints]);
+
+ this.controlPoints = controlPointArray.data;
+ this.controlInds = new int[numPoints][];
+ this.controlFactors = new float[numPoints][];
+ this.inds = new int[numPoints][];
+ this.factors = new float[numPoints][];
+
+ this.boundaryMode = boundaryMode;
+ }
+
+
+ public int addFacePoint(int[] vertices) {
+ controlInds[currPoint] = vertices;
+ controlFactors[currPoint] = new float[vertices.length];
+ Arrays.fill(controlFactors[currPoint], 1.0f / vertices.length);
+
+ inds[currPoint] = new int[0];
+ factors[currPoint] = new float[0];
+
+ return currPoint++;
+ }
+
+ public int addEdgePoint(int[] facePoints, int fromPoint, int toPoint, boolean isBoundary) {
+ if (isBoundary) {
+ controlInds[currPoint] = new int[]{fromPoint, toPoint};
+ controlFactors[currPoint] = new float[]{0.5f, 0.5f};
+
+ inds[currPoint] = new int[0];
+ factors[currPoint] = new float[0];
+ } else {
+ int n = facePoints.length + 2;
+ controlInds[currPoint] = new int[]{fromPoint, toPoint};
+ controlFactors[currPoint] = new float[]{1.0f / n, 1.0f / n};
+
+ inds[currPoint] = facePoints;
+ factors[currPoint] = new float[facePoints.length];
+ Arrays.fill(factors[currPoint], 1.0f / n);
+ }
+ return currPoint++;
+ }
+
+ public int addControlPoint(int[] facePoints, int[] edgePoints, int[] fromEdgePoints, int[] toEdgePoints, boolean[] isEdgeBoundary, int origPoint, boolean isBoundary, boolean hasInternalEdge) {
+ if (isBoundary) {
+ if ((boundaryMode == SubdivisionMesh.BoundaryMode.CREASE_EDGES) || hasInternalEdge) {
+ controlInds[currPoint] = new int[]{origPoint};
+ controlFactors[currPoint] = new float[]{0.5f};
+
+ int numBoundaryEdges = 0;
+ for (int i = 0; i < edgePoints.length; i++) {
+ if (isEdgeBoundary[i]) {
+ numBoundaryEdges++;
+ }
+ }
+ inds[currPoint] = new int[numBoundaryEdges];
+ factors[currPoint] = new float[numBoundaryEdges];
+ int boundaryEdgeInd = 0;
+ for (int i = 0; i < edgePoints.length; i++) {
+ if (isEdgeBoundary[i]) {
+ inds[currPoint][boundaryEdgeInd] = edgePoints[i];
+ factors[currPoint][boundaryEdgeInd] = 0.25f;
+ boundaryEdgeInd++;
+ }
+ }
+ } else {
+ controlInds[currPoint] = new int[]{origPoint};
+ controlFactors[currPoint] = new float[]{1.0f};
+
+ inds[currPoint] = new int[0];
+ factors[currPoint] = new float[0];
+ }
+ } else {
+ int n = facePoints.length;
+
+ controlInds[currPoint] = new int[1 + edgePoints.length * 2];
+ controlFactors[currPoint] = new float[1 + edgePoints.length * 2];
+ controlInds[currPoint][0] = origPoint;
+ controlFactors[currPoint][0] = (n - 3.0f) / n;
+ for (int i = 0; i < edgePoints.length; i++) {
+ controlInds[currPoint][1 + 2 * i] = fromEdgePoints[i];
+ controlFactors[currPoint][1 + 2 * i] = 1.0f / (n * n);
+ controlInds[currPoint][1 + 2 * i + 1] = toEdgePoints[i];
+ controlFactors[currPoint][1 + 2 * i + 1] = 1.0f / (n * n);
+ }
+
+ inds[currPoint] = facePoints;
+ factors[currPoint] = new float[facePoints.length];
+ Arrays.fill(factors[currPoint], 1.0f / (n * n));
+ }
+ return currPoint++;
+ }
+
+ @Override
+ public void update() {
+ int ci;
+ float f;
+ float x, y, z;
+ for (int i = 0; i < numPoints; i++) {
+ x = y = z = 0.0f;
+ for (int j = 0; j < controlInds[i].length; j++) {
+ ci = 3 * controlInds[i][j];
+ f = controlFactors[i][j];
+ x += controlPoints[ci] * f;
+ y += controlPoints[ci + 1] * f;
+ z += controlPoints[ci + 2] * f;
+ }
+ for (int j = 0; j < inds[i].length; j++) {
+ ci = 3 * inds[i][j];
+ f = factors[i][j];
+ x += data[ci] * f;
+ y += data[ci + 1] * f;
+ z += data[ci + 2] * f;
+ }
+ data[3 * i] = x;
+ data[3 * i + 1] = y;
+ data[3 * i + 2] = z;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/java/modelviewer/polygonalmesh/SubdivisionMesh.java b/src/java/modelviewer/polygonalmesh/SubdivisionMesh.java
new file mode 100644
index 0000000..9c8c56f
--- /dev/null
+++ b/src/java/modelviewer/polygonalmesh/SubdivisionMesh.java
@@ -0,0 +1,191 @@
+package modelviewer.polygonalmesh;
+
+/*
+ * Copyright (c) 2010, 2014, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - 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.
+ * - Neither the name of Oracle Corporation 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.
+ */
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Catmull Clark subdivision surface polygon mesh
+ */
+public class SubdivisionMesh extends PolygonMesh {
+ private final PolygonMesh originalMesh;
+ private int subdivisionLevel;
+ private BoundaryMode boundaryMode;
+ private MapBorderMode mapBorderMode;
+ private final List symbolicMeshes;
+
+ private boolean pointValuesDirty;
+ private boolean meshDirty;
+ private boolean subdivisionLevelDirty;
+
+ /**
+ * Describes whether the edges and points at the boundary are treated as creases
+ */
+ public enum BoundaryMode {
+ /**
+ * Only edges at the boundary are treated as creases
+ */
+ CREASE_EDGES,
+ /**
+ * Edges and points at the boundary are treated as creases
+ */
+ CREASE_ALL
+ }
+
+ /**
+ * Describes how the new texture coordinate for the control point is defined
+ */
+ public enum MapBorderMode {
+ /**
+ * Jeeps the same uvs for all control points
+ */
+ NOT_SMOOTH,
+ /**
+ * Smooths uvs of points at corners
+ */
+ SMOOTH_INTERNAL,
+ /**
+ * Smooths uvs of points at boundaries and original control points (and creases [in the future when creases are defined])
+ */
+ SMOOTH_ALL
+ }
+
+ public SubdivisionMesh(PolygonMesh originalMesh, int subdivisionLevel, BoundaryMode boundaryMode, MapBorderMode mapBorderMode) {
+ this.originalMesh = originalMesh;
+ setSubdivisionLevelForced(subdivisionLevel);
+ setBoundaryModeForced(boundaryMode);
+ setMapBorderModeForced(mapBorderMode);
+
+ symbolicMeshes = new ArrayList<>(4); // the polymesh is usually subdivided up to 3 times
+
+ originalMesh.getPoints().addListener((observableArray, sizeChanged, from, to) -> {
+ if (sizeChanged) {
+ meshDirty = true;
+ } else {
+ pointValuesDirty = true;
+ }
+ });
+ originalMesh.getTexCoords().addListener((observableArray, sizeChanged, from, to) -> meshDirty = true);
+ }
+
+ /**
+ * Updates the variables of the underlying polygon mesh.
+ * It only updates the fields that need to be updated.
+ */
+ public void update() {
+ if (meshDirty) {
+ symbolicMeshes.clear();
+ symbolicMeshes.add(new SymbolicPolygonMesh(originalMesh));
+ pointValuesDirty = true;
+ subdivisionLevelDirty = true;
+ }
+
+ while (subdivisionLevel >= symbolicMeshes.size()) {
+ symbolicMeshes.add(SymbolicSubdivisionBuilder.subdivide(symbolicMeshes.get(symbolicMeshes.size()-1), boundaryMode, mapBorderMode));
+ pointValuesDirty = true;
+ subdivisionLevelDirty = true;
+ }
+
+ if (pointValuesDirty) {
+ for (int i = 0; i <= subdivisionLevel; i++) {
+ SymbolicPolygonMesh symbolicMesh = symbolicMeshes.get(i);
+ symbolicMesh.points.update();
+ }
+ }
+
+ if (pointValuesDirty || subdivisionLevelDirty) {
+ getPoints().setAll(symbolicMeshes.get(subdivisionLevel).points.data);
+ }
+
+ if (subdivisionLevelDirty) {
+ faces = symbolicMeshes.get(subdivisionLevel).faces;
+ numEdgesInFaces = -1;
+ getFaceSmoothingGroups().setAll(symbolicMeshes.get(subdivisionLevel).faceSmoothingGroups);
+ getTexCoords().setAll(symbolicMeshes.get(subdivisionLevel).texCoords);
+ }
+
+ meshDirty = false;
+ pointValuesDirty = false;
+ subdivisionLevelDirty = false;
+ }
+
+ private void setSubdivisionLevelForced(int subdivisionLevel) {
+ this.subdivisionLevel = subdivisionLevel;
+ subdivisionLevelDirty = true;
+ }
+
+ private void setBoundaryModeForced(BoundaryMode boundaryMode) {
+ this.boundaryMode = boundaryMode;
+ meshDirty = true;
+ }
+
+ private void setMapBorderModeForced(MapBorderMode mapBorderMode) {
+ this.mapBorderMode = mapBorderMode;
+ meshDirty = true;
+ }
+
+ public PolygonMesh getOriginalMesh() {
+ return originalMesh;
+ }
+
+ public int getSubdivisionLevel() {
+ return subdivisionLevel;
+ }
+
+ public void setSubdivisionLevel(int subdivisionLevel) {
+ if (subdivisionLevel != this.subdivisionLevel) {
+ setSubdivisionLevelForced(subdivisionLevel);
+ }
+ }
+
+ public BoundaryMode getBoundaryMode() {
+ return boundaryMode;
+ }
+
+ public void setBoundaryMode(BoundaryMode boundaryMode) {
+ if (boundaryMode != this.boundaryMode) {
+ setBoundaryModeForced(boundaryMode);
+ }
+ }
+
+ public MapBorderMode getMapBorderMode() {
+ return mapBorderMode;
+ }
+
+ public void setMapBorderMode(MapBorderMode mapBorderMode) {
+ if (mapBorderMode != this.mapBorderMode) {
+ setMapBorderModeForced(mapBorderMode);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/java/modelviewer/polygonalmesh/SymbolicPointArray.java b/src/java/modelviewer/polygonalmesh/SymbolicPointArray.java
new file mode 100644
index 0000000..60fa027
--- /dev/null
+++ b/src/java/modelviewer/polygonalmesh/SymbolicPointArray.java
@@ -0,0 +1,55 @@
+package modelviewer.polygonalmesh;
+
+/*
+ * Copyright (c) 2010, 2014, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - 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.
+ * - Neither the name of Oracle Corporation 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.
+ */
+
+/**
+ * A 3D geometric point array that has the x, y, z coordinates of every point
+ * as a function of other variables.
+ */
+public abstract class SymbolicPointArray {
+ final public float[] data;
+ final public int numPoints;
+ // x, y, z as stated.
+ static final int NUM_COMPONENTS_PER_POINT = 3;
+
+ protected SymbolicPointArray(float[] data) {
+ this.data = data;
+ this.numPoints = data.length / NUM_COMPONENTS_PER_POINT;
+ }
+
+ /**
+ * Updates the variables x, y, z based on the state of the other variables
+ * that this symbolic point depends on.
+ */
+ public abstract void update();
+}
\ No newline at end of file
diff --git a/src/java/modelviewer/polygonalmesh/SymbolicPolygonMesh.java b/src/java/modelviewer/polygonalmesh/SymbolicPolygonMesh.java
new file mode 100644
index 0000000..352cb58
--- /dev/null
+++ b/src/java/modelviewer/polygonalmesh/SymbolicPolygonMesh.java
@@ -0,0 +1,70 @@
+package modelviewer.polygonalmesh;
+
+/*
+ * Copyright (c) 2010, 2014, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - 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.
+ * - Neither the name of Oracle Corporation 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.
+ */
+
+/**
+ * Polygon mesh where the points are symbolic. That is, the values of the
+ * points depend on other variables and they can be updated appropriately.
+ */
+public class SymbolicPolygonMesh {
+ public SymbolicPointArray points;
+ public float[] texCoords;
+ public int[][] faces;
+ public int[] faceSmoothingGroups;
+ private int numEdgesInFaces = -1;
+
+ public SymbolicPolygonMesh(SymbolicPointArray points, float[] texCoords, int[][] faces, int[] faceSmoothingGroups) {
+ this.points = points;
+ this.texCoords = texCoords;
+ this.faces = faces;
+ this.faceSmoothingGroups = faceSmoothingGroups;
+ }
+
+ public SymbolicPolygonMesh(PolygonMesh mesh) {
+ this.points = new OriginalPointArray(mesh);
+ this.texCoords = mesh.getTexCoords().toArray(this.texCoords);
+ this.faces = mesh.faces;
+ this.faceSmoothingGroups = mesh.getFaceSmoothingGroups().toArray(null);
+ }
+
+ public int getNumEdgesInFaces() {
+ if (numEdgesInFaces == -1) {
+ numEdgesInFaces = 0;
+ for(int[] face : faces) {
+ numEdgesInFaces += face.length;
+ }
+ numEdgesInFaces /= 2;
+ }
+ return numEdgesInFaces;
+ }
+}
diff --git a/src/java/modelviewer/polygonalmesh/SymbolicSubdivisionBuilder.java b/src/java/modelviewer/polygonalmesh/SymbolicSubdivisionBuilder.java
new file mode 100644
index 0000000..fc2bd70
--- /dev/null
+++ b/src/java/modelviewer/polygonalmesh/SymbolicSubdivisionBuilder.java
@@ -0,0 +1,393 @@
+package modelviewer.polygonalmesh;
+
+/*
+ * Copyright (c) 2010, 2014, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - 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.
+ * - Neither the name of Oracle Corporation 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 THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.
+ */
+
+
+import javafx.geometry.Point2D;
+
+import java.util.*;
+
+/**
+ * Data structure builder for Catmull Clark subdivision surface
+ */
+public class SymbolicSubdivisionBuilder {
+
+ private SymbolicPolygonMesh oldMesh;
+ private Map edgeInfos;
+ private FaceInfo[] faceInfos;
+ private PointInfo[] pointInfos;
+ private SubdividedPointArray points;
+ private float[] texCoords;
+ private int[] reindex;
+ private int newTexCoordIndex;
+ private SubdivisionMesh.BoundaryMode boundaryMode;
+ private SubdivisionMesh.MapBorderMode mapBorderMode;
+
+ public SymbolicSubdivisionBuilder(SymbolicPolygonMesh oldMesh, SubdivisionMesh.BoundaryMode boundaryMode, SubdivisionMesh.MapBorderMode mapBorderMode) {
+ this.oldMesh = oldMesh;
+ this.boundaryMode = boundaryMode;
+ this.mapBorderMode = mapBorderMode;
+ }
+
+ public SymbolicPolygonMesh subdivide() {
+ collectInfo();
+
+ texCoords = new float[(oldMesh.getNumEdgesInFaces() * 3 + oldMesh.faces.length) * 2];
+ int[][] faces = new int[oldMesh.getNumEdgesInFaces()][8];
+ int[] faceSmoothingGroups = new int[oldMesh.getNumEdgesInFaces()];
+ newTexCoordIndex = 0;
+ reindex = new int[oldMesh.points.numPoints]; // indexes incremented by 1, 0 reserved for empty
+
+ // face points first
+ int newFacesInd = 0;
+ for (int f = 0; f < oldMesh.faces.length; f++) {
+ FaceInfo faceInfo = faceInfos[f];
+ int[] oldFaces = oldMesh.faces[f];
+ for (int p = 0; p < oldFaces.length; p += 2) {
+ faces[newFacesInd][4] = getPointNewIndex(faceInfo);
+ faces[newFacesInd][5] = getTexCoordNewIndex(faceInfo);
+ faceSmoothingGroups[newFacesInd] = oldMesh.faceSmoothingGroups[f];
+ newFacesInd++;
+ }
+ }
+ // then, add edge points
+ newFacesInd = 0;
+ for (int f = 0; f < oldMesh.faces.length; f++) {
+ FaceInfo faceInfo = faceInfos[f];
+ int[] oldFaces = oldMesh.faces[f];
+ for (int p = 0; p < oldFaces.length; p += 2) {
+ faces[newFacesInd][2] = getPointNewIndex(faceInfo, (p / 2 + 1) % faceInfo.edges.length);
+ faces[newFacesInd][3] = getTexCoordNewIndex(faceInfo, (p / 2 + 1) % faceInfo.edges.length);
+ faces[newFacesInd][6] = getPointNewIndex(faceInfo, p / 2);
+ faces[newFacesInd][7] = getTexCoordNewIndex(faceInfo, p / 2);
+ newFacesInd++;
+ }
+ }
+ // finally, add control points
+ newFacesInd = 0;
+ for (int f = 0; f < oldMesh.faces.length; f++) {
+ FaceInfo faceInfo = faceInfos[f];
+ int[] oldFaces = oldMesh.faces[f];
+ for (int p = 0; p < oldFaces.length; p += 2) {
+ faces[newFacesInd][0] = getPointNewIndex(oldFaces[p]);
+ faces[newFacesInd][1] = getTexCoordNewIndex(faceInfo, oldFaces[p], oldFaces[p + 1]);
+ newFacesInd++;
+ }
+ }
+
+ SymbolicPolygonMesh newMesh = new SymbolicPolygonMesh(points, texCoords, faces, faceSmoothingGroups);
+ return newMesh;
+ }
+
+ public static SymbolicPolygonMesh subdivide(SymbolicPolygonMesh oldMesh, SubdivisionMesh.BoundaryMode boundaryMode, SubdivisionMesh.MapBorderMode mapBorderMode) {
+ SymbolicSubdivisionBuilder subdivision = new SymbolicSubdivisionBuilder(oldMesh, boundaryMode, mapBorderMode);
+ return subdivision.subdivide();
+ }
+
+ private void addEdge(Edge edge, FaceInfo faceInfo) {
+ EdgeInfo edgeInfo = edgeInfos.get(edge);
+ if (edgeInfo == null) {
+ edgeInfo = new EdgeInfo();
+ edgeInfo.edge = edge;
+ edgeInfos.put(edge, edgeInfo);
+ }
+ edgeInfo.faces.add(faceInfo);
+ }
+
+ private void addPoint(int point, FaceInfo faceInfo, Edge edge) {
+ PointInfo pointInfo = pointInfos[point];
+ if (pointInfo == null) {
+ pointInfo = new PointInfo();
+ pointInfos[point] = pointInfo;
+ }
+ pointInfo.edges.add(edge);
+ pointInfo.faces.add(faceInfo);
+ }
+
+ private void addPoint(int point, Edge edge) {
+ PointInfo pointInfo = pointInfos[point];
+ if (pointInfo == null) {
+ pointInfo = new PointInfo();
+ pointInfos[point] = pointInfo;
+ }
+ pointInfo.edges.add(edge);
+ }
+
+ private void collectInfo() {
+ edgeInfos = new HashMap<>(oldMesh.faces.length * 2);
+ faceInfos = new FaceInfo[oldMesh.faces.length];
+ pointInfos = new PointInfo[oldMesh.points.numPoints];
+
+ for (int f = 0; f < oldMesh.faces.length; f++) {
+ int[] face = oldMesh.faces[f];
+ int n = face.length / 2;
+ FaceInfo faceInfo = new FaceInfo(n);
+ faceInfos[f] = faceInfo;
+ if (n < 3) {
+ continue;
+ }
+ int from = face[(n - 1) * 2];
+ int texFrom = face[(n - 1) * 2 + 1];
+ double fu, fv;
+ double tu, tv;
+ double u = 0, v = 0;
+ fu = oldMesh.texCoords[texFrom * 2];
+ fv = oldMesh.texCoords[texFrom * 2 + 1];
+ for (int i = 0; i < n; i++) {
+ int to = face[i * 2];
+ int texTo = face[i * 2 + 1];
+ tu = oldMesh.texCoords[texTo * 2];
+ tv = oldMesh.texCoords[texTo * 2 + 1];
+ Point2D midTexCoord = new Point2D((fu + tu) / 2, (fv + tv) / 2);
+ Edge edge = new Edge(from, to);
+ faceInfo.edges[i] = edge;
+ faceInfo.edgeTexCoords[i] = midTexCoord;
+ addEdge(edge, faceInfo);
+ addPoint(to, faceInfo, edge);
+ addPoint(from, edge);
+ fu = tu;
+ fv = tv;
+ u += tu / n;
+ v += tv / n;
+ from = to;
+ texFrom = texTo;
+ }
+ faceInfo.texCoord = new Point2D(u, v);
+ }
+
+ points = new SubdividedPointArray(oldMesh.points, oldMesh.points.numPoints + faceInfos.length + edgeInfos
+ .size(), boundaryMode);
+
+ for (int f = 0; f < oldMesh.faces.length; f++) {
+ int[] face = oldMesh.faces[f];
+ int n = face.length / 2;
+ int[] faceVertices = new int[n];
+ for (int i = 0; i < n; i++) {
+ faceVertices[i] = face[i * 2];
+ }
+ faceInfos[f].facePoint = points.addFacePoint(faceVertices);
+ }
+
+ for (EdgeInfo edgeInfo : edgeInfos.values()) {
+ int[] edgeFacePoints = new int[edgeInfo.faces.size()];
+ for (int f = 0; f < edgeInfo.faces.size(); f++) {
+ edgeFacePoints[f] = edgeInfo.faces.get(f).facePoint;
+ }
+ edgeInfo.edgePoint = points.addEdgePoint(edgeFacePoints, edgeInfo.edge.from, edgeInfo.edge.to, edgeInfo
+ .isBoundary());
+ }
+ }
+
+ private int calcControlPoint(int srcPointIndex) {
+ PointInfo pointInfo = pointInfos[srcPointIndex];
+ int origPoint = srcPointIndex;
+
+ int[] facePoints = new int[pointInfo.faces.size()];
+ for (int f = 0; f < facePoints.length; f++) {
+ facePoints[f] = pointInfo.faces.get(f).facePoint;
+ }
+ int[] edgePoints = new int[pointInfo.edges.size()];
+ boolean[] isEdgeBoundary = new boolean[pointInfo.edges.size()];
+ int[] fromEdgePoints = new int[pointInfo.edges.size()];
+ int[] toEdgePoints = new int[pointInfo.edges.size()];
+ int i = 0;
+ for (Edge edge : pointInfo.edges) {
+ EdgeInfo edgeInfo = edgeInfos.get(edge);
+ edgePoints[i] = edgeInfo.edgePoint;
+ isEdgeBoundary[i] = edgeInfo.isBoundary();
+ fromEdgePoints[i] = edgeInfo.edge.from;
+ toEdgePoints[i] = edgeInfo.edge.to;
+ i++;
+ }
+ int destPointIndex = points.addControlPoint(facePoints, edgePoints, fromEdgePoints, toEdgePoints, isEdgeBoundary, origPoint, pointInfo
+ .isBoundary(), pointInfo.hasInternalEdge());
+ return destPointIndex;
+ }
+
+ private void calcControlTexCoord(FaceInfo faceInfo, int srcPointIndex, int srcTexCoordIndex, int destTexCoordIndex) {
+ PointInfo pointInfo = pointInfos[srcPointIndex];
+ boolean pointBelongsToCrease = oldMesh.points instanceof OriginalPointArray;
+ if ((mapBorderMode == SubdivisionMesh.MapBorderMode.SMOOTH_ALL && (pointInfo.isBoundary() || pointBelongsToCrease)) ||
+ (mapBorderMode == SubdivisionMesh.MapBorderMode.SMOOTH_INTERNAL && !pointInfo.hasInternalEdge())) {
+ double u = oldMesh.texCoords[srcTexCoordIndex * 2] / 2;
+ double v = oldMesh.texCoords[srcTexCoordIndex * 2 + 1] / 2;
+ for (int i = 0; i < faceInfo.edges.length; i++) {
+ if ((faceInfo.edges[i].to == srcPointIndex) || (faceInfo.edges[i].from == srcPointIndex)) {
+ u += faceInfo.edgeTexCoords[i].getX() / 4;
+ v += faceInfo.edgeTexCoords[i].getY() / 4;
+ }
+ }
+ texCoords[destTexCoordIndex * 2] = (float) u;
+ texCoords[destTexCoordIndex * 2 + 1] = (float) v;
+ } else {
+ texCoords[destTexCoordIndex * 2] = oldMesh.texCoords[srcTexCoordIndex * 2];
+ texCoords[destTexCoordIndex * 2 + 1] = oldMesh.texCoords[srcTexCoordIndex * 2 + 1];
+ }
+ }
+
+ private int getPointNewIndex(int srcPointIndex) {
+ int destPointIndex = reindex[srcPointIndex] - 1;
+ if (destPointIndex == -1) {
+ destPointIndex = calcControlPoint(srcPointIndex);
+ reindex[srcPointIndex] = destPointIndex + 1;
+ }
+ return destPointIndex;
+ }
+
+ private int getPointNewIndex(FaceInfo faceInfo, int edgeInd) {
+ Edge edge = faceInfo.edges[edgeInd];
+ EdgeInfo edgeInfo = edgeInfos.get(edge);
+ return edgeInfo.edgePoint;
+ }
+
+ private int getPointNewIndex(FaceInfo faceInfo) {
+ return faceInfo.facePoint;
+ }
+
+ private int getTexCoordNewIndex(FaceInfo faceInfo, int srcPointIndex, int srcTexCoordIndex) {
+ int destTexCoordIndex = newTexCoordIndex;
+ newTexCoordIndex++;
+ calcControlTexCoord(faceInfo, srcPointIndex, srcTexCoordIndex, destTexCoordIndex);
+ return destTexCoordIndex;
+ }
+
+ private int getTexCoordNewIndex(FaceInfo faceInfo, int edgeInd) {
+ int destTexCoordIndex = newTexCoordIndex;
+ newTexCoordIndex++;
+ texCoords[destTexCoordIndex * 2] = (float) faceInfo.edgeTexCoords[edgeInd].getX();
+ texCoords[destTexCoordIndex * 2 + 1] = (float) faceInfo.edgeTexCoords[edgeInd].getY();
+ return destTexCoordIndex;
+ }
+
+ private int getTexCoordNewIndex(FaceInfo faceInfo) {
+ int destTexCoordIndex = faceInfo.newTexCoordIndex - 1;
+ if (destTexCoordIndex == -1) {
+ destTexCoordIndex = newTexCoordIndex;
+ faceInfo.newTexCoordIndex = destTexCoordIndex + 1;
+ newTexCoordIndex++;
+ texCoords[destTexCoordIndex * 2] = (float) faceInfo.texCoord.getX();
+ texCoords[destTexCoordIndex * 2 + 1] = (float) faceInfo.texCoord.getY();
+ }
+ return destTexCoordIndex;
+ }
+
+ private static class Edge {
+ int from, to;
+
+ public Edge(int from, int to) {
+ this.from = Math.min(from, to);
+ this.to = Math.max(from, to);
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 7;
+ hash = 41 * hash + this.from;
+ hash = 41 * hash + this.to;
+ return hash;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final Edge other = (Edge) obj;
+ if (this.from != other.from) {
+ return false;
+ }
+ if (this.to != other.to) {
+ return false;
+ }
+ return true;
+ }
+ }
+
+ private static class EdgeInfo {
+ Edge edge;
+ int edgePoint;
+ List faces = new ArrayList<>(2);
+
+ /**
+ * an edge is in the boundary if it has only one adjacent face
+ */
+ public boolean isBoundary() {
+ return faces.size() == 1;
+ }
+ }
+
+ private class PointInfo {
+ List faces = new ArrayList<>(4);
+ Set edges = new HashSet<>(4);
+
+ /**
+ * A point is in the boundary if any of its adjacent edges is in the boundary
+ */
+ public boolean isBoundary() {
+ for (Edge edge : edges) {
+ EdgeInfo edgeInfo = edgeInfos.get(edge);
+ if (edgeInfo.isBoundary())
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * A point is internal if at least one of its adjacent edges is not in the boundary
+ */
+ public boolean hasInternalEdge() {
+ for (Edge edge : edges) {
+ EdgeInfo edgeInfo = edgeInfos.get(edge);
+ if (!edgeInfo.isBoundary())
+ return true;
+ }
+ return false;
+ }
+ }
+
+ private static class FaceInfo {
+ int facePoint;
+ Point2D texCoord;
+ int newTexCoordIndex;
+ Edge[] edges;
+ Point2D[] edgeTexCoords;
+
+ public FaceInfo(int n) {
+ edges = new Edge[n];
+ edgeTexCoords = new Point2D[n];
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/java/modelviewer/rs/Model.java b/src/java/modelviewer/rs/Model.java
new file mode 100644
index 0000000..bd26874
--- /dev/null
+++ b/src/java/modelviewer/rs/Model.java
@@ -0,0 +1,759 @@
+package modelviewer.rs;
+
+import javafx.geometry.Point3D;
+import javafx.scene.paint.Color;
+import lombok.SneakyThrows;
+import modelviewer.rs.buffer.Buffer;
+
+public class Model {
+
+ public static Color rs2HSBToColor(short hsb, int alpha) {
+
+ int transparency = alpha;
+ if (transparency <= 0) {
+ transparency = 255;
+ }
+
+ int hue = hsb >> 10 & 0x3f;
+ int sat = hsb >> 7 & 0x07;
+ int bri = hsb & 0x7f;
+ java.awt.Color awtCol = java.awt.Color.getHSBColor((float) hue / 63, (float) sat / 7, (float) bri / 127);
+ double r = awtCol.getRed() / 255.0;
+ double g = awtCol.getGreen() / 255.0;
+ double b = awtCol.getBlue() / 255.0;
+ return Color.color(r, g, b, transparency / 255.0);
+ }
+
+ public boolean uvBetween(int face, double uLower, double uUpper, double vLower, double vUpper) {
+ float u1 = textureUCoordinates[face][0];
+ float u2 = textureUCoordinates[face][1];
+ float u3 = textureUCoordinates[face][2];
+
+ float v1 = textureVCoordinates[face][0];
+ float v2 = textureVCoordinates[face][1];
+ float v3 = textureVCoordinates[face][2];
+ return (u1 >= uLower && u1 <= uUpper) && (u2 >= uLower && u2 <= uUpper) && (u3 >= uLower && u3 <= uUpper) && (v1 >= vLower && v1 <= vUpper) && (v2 >= vLower && v2 <= vUpper) && (v3 >= vLower && v3 <= vUpper);
+ }
+
+
+ public static Model decode(byte[] data, int id) {
+ Model model = new Model();
+ try {
+ if (data[data.length - 1] == -1 && data[data.length - 2] == -1) {
+ model.decodeNew(data, id);
+ } else {
+ model.decode317(data, id);
+ }
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ /* System.out.println(Arrays.toString(model.verticesXCoordinate));
+ System.out.println(Arrays.toString(model.verticesYCoordinate));
+ System.out.println(Arrays.toString(model.verticesZCoordinate));
+ System.out.println("-".repeat(250));
+ System.out.println(Arrays.toString(model.faceIndicesA));
+ System.out.println(Arrays.toString(model.faceIndicesB));
+ System.out.println(Arrays.toString(model.faceIndicesC));*/
+ return model;
+ }
+
+ public void decodeNew(byte[] data, int id) {
+ System.out.println("Decoded new " + id + " | " + data.length);
+ modelId = id;
+ Buffer first = new Buffer(data);
+ Buffer second = new Buffer(data);
+ Buffer third = new Buffer(data);
+ Buffer fourth = new Buffer(data);
+ Buffer fifth = new Buffer(data);
+ Buffer sixth = new Buffer(data);
+ Buffer seventh = new Buffer(data);
+
+ first.pos = data.length - 23;
+ vertices = first.getUnsignedShort();
+ triangleCount = first.getUnsignedShort();
+ texturedFaces = first.getUnsignedByte();
+
+ System.out.println("Vertices: " + vertices);
+ System.out.println("Triangle count: " + triangleCount);
+ System.out.println("Textured faces: " + texturedFaces);
+
+
+ // System.err.println("Vertices: " + vertices + " | Faces: " + faces + " | Texture faces: " + texture_faces);
+
+ int flag = first.getUnsignedByte();//texture flag 00 false, 01+ true
+ boolean hasFaceTypes = (flag & 0x1) == 1;
+ boolean hasParticleEffects = (flag & 0x2) == 2;
+ boolean hasBillboards = (flag & 0x4) == 4;
+ boolean hasVersion = (flag & 0x8) == 8;
+ if (hasVersion) {
+ first.pos -= 7;
+ first.pos += 6;
+ }
+ int model_priority_opcode = first.getUnsignedByte();
+ int model_alpha_opcode = first.getUnsignedByte();
+ int model_muscle_opcode = first.getUnsignedByte();
+ int model_texture_opcode = first.getUnsignedByte();
+ int model_bones_opcode = first.getUnsignedByte();
+ System.out.println("---------------> data here");
+ //619, 1244, 0, true, false, true, true, true
+ System.out.printf("%d, %d, %d, %b, %b, %b, %b, %b\n", vertices, triangleCount, texturedFaces, flag == 1, model_priority_opcode == 1, model_alpha_opcode == 1, model_muscle_opcode == 1, model_bones_opcode == 1);
+ int model_vertex_x = first.getUnsignedShort();
+ int model_vertex_y = first.getUnsignedShort();
+ int model_vertex_z = first.getUnsignedShort();
+ int model_vertex_points = first.getUnsignedShort();
+ System.out.println("new model = " + vertices + ", " + triangleCount + ", " + texturedFaces + ", " + flag + ", " + model_priority_opcode + ", " + model_alpha_opcode + ", " + model_muscle_opcode + ", " + model_bones_opcode + ", " + model_vertex_x + ", " + model_vertex_y + ", " + model_vertex_z + ", " + model_vertex_points);
+ int model_texture_indices = first.getUnsignedShort();
+ int texture_id_simple = 0;
+ int texture_id_complex = 0;
+ int texture_id_cube = 0;
+ int face;
+ System.out.println("Tex faces: " + texturedFaces);
+ if (texturedFaces > 0) {
+ textureMap = new short[texturedFaces];
+ first.pos = 0;
+ for (face = 0; face < texturedFaces; face++) {
+ short opcode = textureMap[face] = first.getSignedByte();
+ if (opcode == 0) {
+ texture_id_simple++;
+ }
+ if (opcode >= 1 && opcode <= 3) {
+ texture_id_complex++;
+ }
+ if (opcode == 2) {
+ texture_id_cube++;
+ }
+
+ }
+ }
+ int pos = texturedFaces;
+
+ int model_vertex_offset = pos;
+ pos += vertices;
+
+ int model_render_type_offset = pos;
+ if (flag == 1)
+ pos += triangleCount;
+
+ int model_face_offset = pos;
+ pos += triangleCount;
+
+ int model_face_priorities_offset = pos;
+ if (model_priority_opcode == 255)
+ pos += triangleCount;
+
+ int model_muscle_offset = pos;
+ if (model_muscle_opcode == 1)
+ pos += triangleCount;
+
+ int model_bones_offset = pos;
+ if (model_bones_opcode == 1)
+ pos += vertices;
+
+ int model_alpha_offset = pos;
+ if (model_alpha_opcode == 1)
+ pos += triangleCount;
+
+ int model_points_offset = pos;
+ pos += model_vertex_points;
+
+ int model_texture_id = pos;
+ if (model_texture_opcode == 1)
+ pos += triangleCount * 2;
+
+ int model_texture_coordinate_offset = pos;
+ pos += model_texture_indices;
+
+ int model_color_offset = pos;
+ pos += triangleCount * 2;
+
+ int model_vertex_x_offset = pos;
+ pos += model_vertex_x;
+
+ int model_vertex_y_offset = pos;
+ pos += model_vertex_y;
+
+ int model_vertex_z_offset = pos;
+ pos += model_vertex_z;
+
+ int model_simple_texture_offset = pos;
+ pos += texture_id_simple * 6;
+
+ int model_complex_texture_offset = pos;
+ pos += texture_id_complex * 6;
+
+ int model_texture_scale_offset = pos;
+ pos += texture_id_complex * 6;
+
+ int model_texture_rotation_offset = pos;
+ pos += texture_id_complex * 2;
+
+ int model_texture_direction_offset = pos;
+ pos += texture_id_complex;
+
+ int model_texture_translate_offset = pos;
+ pos += texture_id_complex * 2 + texture_id_cube * 2;
+
+ verticesXCoordinate = new int[vertices];
+ verticesYCoordinate = new int[vertices];
+ verticesZCoordinate = new int[vertices];
+ faceIndicesA = new int[triangleCount];
+ faceIndicesB = new int[triangleCount];
+ faceIndicesC = new int[triangleCount];
+ if (model_bones_opcode == 1)
+ vertexWeights = new int[vertices];
+
+ if (flag == 1)
+ triangleInfo = new int[triangleCount];
+
+ if (model_priority_opcode == 255)
+ trianglePriorities = new byte[triangleCount];
+ else
+ modelPriority = (byte) model_priority_opcode;
+
+ if (model_alpha_opcode == 1)
+ faceAlpha = new int[triangleCount];
+
+ if (model_muscle_opcode == 1)
+ triangleSkin = new int[triangleCount];
+
+ if (model_texture_opcode == 1)
+ faceMaterial = new short[triangleCount];
+
+ if (model_texture_opcode == 1 && texturedFaces > 0)
+ faceTexture = new short[triangleCount];
+
+ triangleColors = new short[triangleCount];
+ if (texturedFaces > 0) {
+ textureVertexA = new short[texturedFaces];
+ textureVertexB = new short[texturedFaces];
+ textureVertexC = new short[texturedFaces];
+ }
+ first.pos = model_vertex_offset;
+ second.pos = model_vertex_x_offset;
+ third.pos = model_vertex_y_offset;
+ fourth.pos = model_vertex_z_offset;
+ fifth.pos = model_bones_offset;
+ int start_x = 0;
+ int start_y = 0;
+
+ int start_z = 0;
+ for (int point = 0; point < vertices; point++) {
+ int position_mask = first.getUnsignedByte();
+ int x = 0;
+ if ((position_mask & 1) != 0) {
+ x = second.getSignedSmart();
+ }
+ int y = 0;
+ if ((position_mask & 2) != 0) {
+ y = third.getSignedSmart();
+ }
+ int z = 0;
+ if ((position_mask & 4) != 0) {
+ z = fourth.getSignedSmart();
+ }
+ verticesXCoordinate[point] = start_x + x;
+ verticesYCoordinate[point] = start_y + y;
+ verticesZCoordinate[point] = start_z + z;
+ start_x = verticesXCoordinate[point];
+ start_y = verticesYCoordinate[point];
+ start_z = verticesZCoordinate[point];
+ if (vertexWeights != null)
+ vertexWeights[point] = fifth.getUnsignedByte();
+
+ }
+ first.pos = model_color_offset;
+ second.pos = model_render_type_offset;
+ third.pos = model_face_priorities_offset;
+ fourth.pos = model_alpha_offset;
+ fifth.pos = model_muscle_offset;
+ sixth.pos = model_texture_id;
+ seventh.pos = model_texture_coordinate_offset;
+ for (face = 0; face < triangleCount; face++) {
+ triangleColors[face] = (short) (first.getUnsignedShort() & 0xFFFF);
+ // System.out.println("Read face color: " + triangleColors[face]);
+ if (flag == 1) {
+ triangleInfo[face] = second.getSignedByte();
+ }
+ if (model_priority_opcode == 255) {
+ trianglePriorities[face] = third.getSignedByte();
+ }
+ if (model_alpha_opcode == 1) {
+ faceAlpha[face] = fourth.getSignedByte();
+ if (faceAlpha[face] < 0)
+ faceAlpha[face] = (256 + faceAlpha[face]);
+
+ }
+ if (model_muscle_opcode == 1)
+ triangleSkin[face] = fifth.getUnsignedByte();
+
+ if (model_texture_opcode == 1) {
+ //System.out.println("Started reading face material at pos " + sixth.pos);
+ faceMaterial[face] = (short) (sixth.getUnsignedShort() - 1);
+ if (faceMaterial[face] >= 0) {
+ if (triangleInfo != null) {
+ if (triangleInfo[face] < 2
+ && triangleColors[face] != 127
+ && triangleColors[face] != -27075
+ && triangleColors[face] != 8128
+ && triangleColors[face] != 7510) {
+ faceMaterial[face] = -1;
+ }
+ }
+ }
+
+
+ if (faceMaterial[face] != -1 && faceMaterial[face] >= 0 && faceMaterial[face] <= 85)
+ triangleColors[face] = 127;
+
+ }
+
+ //System.out.println(Arrays.toString(triangleColors));
+ if (faceTexture != null && faceMaterial[face] != -1) {
+ faceTexture[face] = (byte) (seventh.getUnsignedByte() - 1);
+ // System.out.println(faceTexture[face] + " ->>>>>>");
+ }
+ }
+ first.pos = model_points_offset;
+ second.pos = model_face_offset;
+ int a = 0;
+ int b = 0;
+ int c = 0;
+ int last_coordinate = 0;
+ for (face = 0; face < triangleCount; face++) {
+ int opcode = second.getUnsignedByte();
+ if (opcode == 1) {
+ a = first.getSignedSmart() + last_coordinate;
+ last_coordinate = a;
+ b = first.getSignedSmart() + last_coordinate;
+ last_coordinate = b;
+ c = first.getSignedSmart() + last_coordinate;
+ last_coordinate = c;
+ faceIndicesA[face] = a;
+ faceIndicesB[face] = b;
+ faceIndicesC[face] = c;
+ }
+ if (opcode == 2) {
+ b = c;
+ c = first.getSignedSmart() + last_coordinate;
+ last_coordinate = c;
+ faceIndicesA[face] = a;
+ faceIndicesB[face] = b;
+ faceIndicesC[face] = c;
+ }
+ if (opcode == 3) {
+ a = c;
+ c = first.getSignedSmart() + last_coordinate;
+ last_coordinate = c;
+ faceIndicesA[face] = a;
+ faceIndicesB[face] = b;
+ faceIndicesC[face] = c;
+ }
+ if (opcode == 4) {
+ int l14 = a;
+ a = b;
+ b = l14;
+ c = first.getSignedSmart() + last_coordinate;
+ last_coordinate = c;
+ faceIndicesA[face] = a;
+ faceIndicesB[face] = b;
+ faceIndicesC[face] = c;
+ }
+ }
+ first.pos = model_simple_texture_offset;
+ second.pos = model_complex_texture_offset;
+ third.pos = model_texture_scale_offset;
+ fourth.pos = model_texture_rotation_offset;
+ fifth.pos = model_texture_direction_offset;
+ sixth.pos = model_texture_translate_offset;
+ for (face = 0; face < texturedFaces; face++) {
+ int opcode = textureMap[face] & 0xff;
+ if (opcode == 0) {
+ textureVertexA[face] = (short) first.getUnsignedShort();
+ textureVertexB[face] = (short) first.getUnsignedShort();
+ textureVertexC[face] = (short) first.getUnsignedShort();
+ }
+ if (opcode == 1) {
+ textureVertexA[face] = (short) second.getUnsignedShort();
+ textureVertexB[face] = (short) second.getUnsignedShort();
+ textureVertexC[face] = (short) second.getUnsignedShort();
+ }
+ if (opcode == 2) {
+ textureVertexA[face] = (short) second.getUnsignedShort();
+ textureVertexB[face] = (short) second.getUnsignedShort();
+ textureVertexC[face] = (short) second.getUnsignedShort();
+ }
+ if (opcode == 3) {
+ textureVertexA[face] = (short) second.getUnsignedShort();
+ textureVertexB[face] = (short) second.getUnsignedShort();
+ textureVertexC[face] = (short) second.getUnsignedShort();
+ }
+ }
+
+ }
+
+ //*Added*//
+ public short[] faceMaterial;
+ public short[] faceTexture;
+ public short[] textureMap;
+
+
+ public void computeUVCoordinates() {
+ if (texturedFaces == 0) {
+ return;
+ }
+
+ textureUCoordinates = new float[triangleCount][];
+ textureVCoordinates = new float[triangleCount][];
+
+ for (int i = 0; i < triangleCount; i++) {
+ int coordinate = triangleInfo == null ? -1 : triangleInfo[i] >> 2;
+ int textureIdx;
+ if (triangleInfo == null || triangleInfo[i] < 2) {
+ textureIdx = -1;
+ } else {
+ textureIdx = triangleColors[i] & 0xFFFF;
+ }
+
+ if (textureIdx != -1) {
+ float[] u = new float[3];
+ float[] v = new float[3];
+
+ if (coordinate == -1) {
+ u[0] = 0.0F;
+ v[0] = 1.0F;
+
+ u[1] = 1.0F;
+ v[1] = 1.0F;
+
+ u[2] = 0.0F;
+ v[2] = 0.0F;
+ } else {
+ coordinate &= 0xFF;
+ int faceA = faceIndicesA[i];
+ int faceB = faceIndicesB[i];
+ int faceC = faceIndicesC[i];
+
+ Point3D a = new Point3D(verticesXCoordinate[faceA], verticesYCoordinate[faceA], verticesZCoordinate[faceA]);
+ Point3D b = new Point3D(verticesXCoordinate[faceB], verticesYCoordinate[faceB], verticesZCoordinate[faceB]);
+ Point3D c = new Point3D(verticesXCoordinate[faceC], verticesYCoordinate[faceC], verticesZCoordinate[faceC]);
+
+ Point3D p = new Point3D(verticesXCoordinate[textureVertexA[coordinate]], verticesYCoordinate[textureVertexA[coordinate]], verticesZCoordinate[textureVertexA[coordinate]]);
+ Point3D m = new Point3D(verticesXCoordinate[textureVertexB[coordinate]], verticesYCoordinate[textureVertexB[coordinate]], verticesZCoordinate[textureVertexB[coordinate]]);
+ Point3D n = new Point3D(verticesXCoordinate[textureVertexC[coordinate]], verticesYCoordinate[textureVertexC[coordinate]], verticesZCoordinate[textureVertexC[coordinate]]);
+
+ Point3D pM = m.subtract(p);
+ Point3D pN = n.subtract(p);
+ Point3D pA = a.subtract(p);
+ Point3D pB = b.subtract(p);
+ Point3D pC = c.subtract(p);
+
+ Point3D pMxPn = pM.crossProduct(pN);
+
+ Point3D uCoordinate = pN.crossProduct(pMxPn);
+ double mU = 1.0F / uCoordinate.dotProduct(pM);
+
+ double uA = uCoordinate.dotProduct(pA) * mU;
+ double uB = uCoordinate.dotProduct(pB) * mU;
+ double uC = uCoordinate.dotProduct(pC) * mU;
+
+ Point3D vCoordinate = pM.crossProduct(pMxPn);
+ double mV = 1.0 / vCoordinate.dotProduct(pN);
+ double vA = vCoordinate.dotProduct(pA) * mV;
+ double vB = vCoordinate.dotProduct(pB) * mV;
+ double vC = vCoordinate.dotProduct(pC) * mV;
+
+ u[0] = (float) uA;
+ u[1] = (float) uB;
+ u[2] = (float) uC;
+
+ v[0] = (float) vA;
+ v[1] = (float) vB;
+ v[2] = (float) vC;
+ }
+ this.textureUCoordinates[i] = u;
+ this.textureVCoordinates[i] = v;
+ }
+ }
+ }
+
+ public float[][] textureUCoordinates;
+ public float[][] textureVCoordinates;
+
+ @SneakyThrows(Exception.class)
+ public void decode317(byte[] data, int id) {
+ System.out.println("Decoded old model " + id + " | " + data.length);
+ modelId = id;
+ Buffer first = new Buffer(data);
+ Buffer second = new Buffer(data);
+ Buffer third = new Buffer(data);
+ Buffer fourth = new Buffer(data);
+ Buffer fifth = new Buffer(data);
+ first.pos = data.length - 18;
+ vertices = first.getUnsignedShort();
+ triangleCount = first.getUnsignedShort();
+ texturedFaces = first.getUnsignedByte();
+
+
+ int renderTypeOpcode = first.getUnsignedByte();
+ int renderPriorityOpcode = first.getUnsignedByte();
+ int triangleAlphaOpcode = first.getUnsignedByte();
+ int triangleSkinOpcode = first.getUnsignedByte();
+ int vertexLabelOpcode = first.getUnsignedByte();
+ int verticesXCoordinateOffset = first.getUnsignedShort();
+
+ int verticesYCoordinateOffset = first.getUnsignedShort();
+ int verticesZCoordinateOffset = first.getUnsignedShort();
+ int triangleIndicesOffset = first.getUnsignedShort();
+
+ int pos = 0;
+
+ int vertexFlagOffset = pos;
+ pos += vertices;
+
+ int triangleCompressTypeOffset = pos;
+ pos += triangleCount;
+
+ int facePriorityOffset = pos;
+ if (renderPriorityOpcode == 255) {
+ pos += triangleCount;
+ }
+
+ int triangleSkinOffset = pos;
+ if (triangleSkinOpcode == 1) {
+ pos += triangleCount;
+ }
+
+ int renderTypeOffset = pos;
+ if (renderTypeOpcode == 1) {
+ pos += triangleCount;
+ }
+
+ int vertexLabelsOffset = pos;
+ if (vertexLabelOpcode == 1) {
+ pos += vertices;
+ }
+
+ int triangleAlphaOffset = pos;
+ if (triangleAlphaOpcode == 1) {
+ pos += triangleCount;
+ }
+
+ int indicesOffset = pos;
+ pos += triangleIndicesOffset;
+
+ int triangleColorOffset = pos;
+ pos += triangleCount * 2;
+
+ int textureOffset = pos;
+ pos += texturedFaces * 6;
+
+ int xOffset = pos;
+ pos += verticesXCoordinateOffset;
+
+ int yOffset = pos;
+ pos += verticesYCoordinateOffset;
+
+ int zOffset = pos;
+
+ verticesXCoordinate = new int[vertices];
+ verticesYCoordinate = new int[vertices];
+ verticesZCoordinate = new int[vertices];
+ faceIndicesA = new int[triangleCount];
+ faceIndicesB = new int[triangleCount];
+ faceIndicesC = new int[triangleCount];
+ if (texturedFaces > 0) {
+ textureVertexA = new short[texturedFaces];
+ textureVertexB = new short[texturedFaces];
+ textureVertexC = new short[texturedFaces];
+ }
+
+ if (vertexLabelOpcode == 1)
+ vertexWeights = new int[vertices];
+
+
+ if (renderTypeOpcode == 1) {
+ triangleInfo = new int[triangleCount];
+ }
+
+ if (renderPriorityOpcode == 255)
+ trianglePriorities = new byte[triangleCount];
+ else
+ modelPriority = (byte) renderPriorityOpcode;
+
+ if (triangleAlphaOpcode == 1)
+ faceAlpha = new int[triangleCount];
+
+ if (triangleSkinOpcode == 1)
+ triangleSkin = new int[triangleCount];
+
+ triangleColors = new short[triangleCount];
+ first.pos = vertexFlagOffset;
+ second.pos = xOffset;
+ third.pos = yOffset;
+ fourth.pos = zOffset;
+ fifth.pos = vertexLabelsOffset; // 18 +
+ int baseX = 0;
+ int baseY = 0;
+ int baseZ = 0;
+
+ for (int point = 0; point < vertices; point++) {
+ int flag = first.getUnsignedByte();
+
+ int x = 0;
+ if ((flag & 0x1) != 0) {
+ x = second.getSignedSmart();
+ }
+
+ int y = 0;
+ if ((flag & 0x2) != 0) {
+ y = third.getSignedSmart();
+ }
+ int z = 0;
+ if ((flag & 0x4) != 0) {
+ z = fourth.getSignedSmart();
+ }
+
+ verticesXCoordinate[point] = baseX + x;
+ verticesYCoordinate[point] = baseY + y;
+ verticesZCoordinate[point] = baseZ + z;
+ baseX = verticesXCoordinate[point];
+ baseY = verticesYCoordinate[point];
+ baseZ = verticesZCoordinate[point];
+ if (vertexLabelOpcode == 1) {
+ vertexWeights[point] = fifth.getUnsignedByte();
+ }
+ }
+
+
+ first.pos = triangleColorOffset;
+ second.pos = renderTypeOffset;
+ third.pos = facePriorityOffset;
+ fourth.pos = triangleAlphaOffset;
+ fifth.pos = triangleSkinOffset;
+
+ for (int face = 0; face < triangleCount; face++) {
+ int color = first.getUnsignedShort();
+ triangleColors[face] = (short) color;
+
+ if (renderTypeOpcode == 1) {
+ triangleInfo[face] = second.getUnsignedByte();
+ }
+ if (renderPriorityOpcode == 255) {
+ trianglePriorities[face] = third.getSignedByte();
+ }
+
+ if (triangleAlphaOpcode == 1) {
+ faceAlpha[face] = fourth.getSignedByte();
+ if (faceAlpha[face] < 0) {
+ faceAlpha[face] = (256 + faceAlpha[face]);
+ }
+
+ }
+ if (triangleSkinOpcode == 1) {
+ triangleSkin[face] = fifth.getUnsignedByte();
+ }
+
+ }
+ first.pos = indicesOffset;
+ second.pos = triangleCompressTypeOffset;
+ int a = 0;
+ int b = 0;
+ int c = 0;
+ int offset = 0;
+ int coordinate;
+
+ for (int face = 0; face < triangleCount; face++) {
+ int opcode = second.getUnsignedByte();
+
+
+ if (opcode == 1) {
+ a = (first.getSignedSmart() + offset);
+ offset = a;
+ b = (first.getSignedSmart() + offset);
+ offset = b;
+ c = (first.getSignedSmart() + offset);
+ offset = c;
+ faceIndicesA[face] = a;
+ faceIndicesB[face] = b;
+ faceIndicesC[face] = c;
+
+ }
+ if (opcode == 2) {
+ b = c;
+ c = (first.getSignedSmart() + offset);
+ offset = c;
+ faceIndicesA[face] = a;
+ faceIndicesB[face] = b;
+ faceIndicesC[face] = c;
+ }
+ if (opcode == 3) {
+ a = c;
+ c = (first.getSignedSmart() + offset);
+ offset = c;
+ faceIndicesA[face] = a;
+ faceIndicesB[face] = b;
+ faceIndicesC[face] = c;
+ }
+ if (opcode == 4) {
+ coordinate = a;
+ a = b;
+ b = coordinate;
+ c = (first.getSignedSmart() + offset);
+ offset = c;
+ faceIndicesA[face] = a;
+ faceIndicesB[face] = b;
+ faceIndicesC[face] = c;
+ }
+
+ }
+ first.pos = textureOffset;
+
+ for (int face = 0; face < texturedFaces; face++) {
+ textureVertexA[face] = (short) first.getUnsignedShort();
+ textureVertexB[face] = (short) first.getUnsignedShort();
+ textureVertexC[face] = (short) first.getUnsignedShort();
+ }
+
+ if (triangleInfo == null) {
+ triangleInfo = new int[triangleCount];
+ }
+ // System.out.println("Tri info = " + Arrays.toString(triangleInfo));
+ }
+
+ private int modelId;
+
+ public int getModelId() {
+ return modelId;
+ }
+
+ public int hsbToRGB(int hsb) {
+ float h = hsb >> 10 & 0x3f;
+ float s = hsb >> 7 & 0x07;
+ float b = hsb & 0x7f;
+ return java.awt.Color.HSBtoRGB(h / 63, s / 7, b / 127);
+ }
+
+
+ public int vertices;
+ public int triangleCount;
+ public int[] verticesXCoordinate;
+ public int[] verticesYCoordinate;
+ public int[] verticesZCoordinate;
+ public int[] faceIndicesA;
+ public int[] faceIndicesB;
+ public int[] faceIndicesC;
+ public int[] triangleInfo;
+ public byte[] trianglePriorities;
+ public int[] faceAlpha;
+ public short[] triangleColors;
+ public byte modelPriority = 0;
+ public int texturedFaces;
+ public short[] textureVertexA;
+ public short[] textureVertexB;
+ public short[] textureVertexC;
+ public int[] vertexWeights;
+ public int[] triangleSkin;
+
+
+}
diff --git a/src/java/modelviewer/rs/buffer/Buffer.java b/src/java/modelviewer/rs/buffer/Buffer.java
new file mode 100644
index 0000000..c4fdbbc
--- /dev/null
+++ b/src/java/modelviewer/rs/buffer/Buffer.java
@@ -0,0 +1,594 @@
+package modelviewer.rs.buffer;
+
+import lombok.SneakyThrows;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+
+
+public final class Buffer {
+
+ @SneakyThrows(Exception.class)
+ public static Buffer create() {
+ synchronized (BUFFER_CACHE) {
+ Buffer buffer = null;
+ if (cache_indices > 0) {
+ cache_indices--;
+
+ buffer = (Buffer) BUFFER_CACHE.pop();
+ }
+ if (buffer != null) {
+ buffer.pos = 0;
+ return buffer;
+ }
+ }
+ Buffer buffer = new Buffer();
+ buffer.pos = 0;
+ buffer.payload = new byte[5000];
+ return buffer;
+ }
+
+ private Buffer() {
+ }
+
+ @SneakyThrows(Exception.class)
+ public Buffer(byte[] payload) {
+ this.payload = payload;
+ this.pos = 0;
+ }
+
+ @SneakyThrows(Exception.class)
+ public Buffer(int length, byte initialValue) {
+ this.payload = new byte[length];
+ Arrays.fill(payload, initialValue);
+ }
+
+ @SneakyThrows(Exception.class)
+ public void skip(int amount) {
+ this.pos += amount;
+ }
+
+ @SneakyThrows(Exception.class)
+ public void setPosition(int pos) {
+ this.pos = pos;
+ }
+
+ @SneakyThrows(Exception.class)
+ public int getSmartShortMinusOne() {
+ final int i_118_ = this.payload[pos++] & 0xFF;
+ if (i_118_ < 128) {
+ return (this.getSignedByte() & 0xFF) - 1;
+ }
+ return (this.getShort() & 0xFFFF) - 32769;
+ }
+
+ /**
+ * Reads a smart value from the buffer.
+ *
+ * @return the read smart value.
+ */
+ @SneakyThrows(Exception.class)
+ public int readSmart() {
+ int value = payload[pos] & 0xff;
+ if (value < 128) {
+ return getUnsignedByte();
+ }
+ return getUnsignedShort() - 32768;
+ }
+
+
+ /**
+ * Reads a smart value from the buffer (supports -1).
+ *
+ * @return the read smart value.
+ */
+ @SneakyThrows(Exception.class)
+ public int readSmartNS() {
+ return readSmart() - 1;
+ }
+
+
+ /**
+ * Writes a smart value to the buffer.
+ *
+ * @param value the value to write.
+ */
+ @SneakyThrows(Exception.class)
+ public void writeSmart(int value) {
+ if (value >= 128) {
+ writeShort(value + 32768);
+ } else {
+ write_byte(value);
+ }
+ }
+
+
+ /**
+ * Reads an unsigned smart value from the buffer.
+ *
+ * @return the read unsigned smart value.
+ */
+ @SneakyThrows(Exception.class)
+ public int readUnsignedSmart() {
+ int value = payload[pos] & 0xff;
+ if (value < 128) {
+ return getUnsignedByte() - 64;
+ }
+ return getUnsignedShort() - 49152;
+ }
+
+
+ /**
+ * Writes an unsigned smart value to the buffer.
+ *
+ * @param value the value to write.
+ */
+ @SneakyThrows(Exception.class)
+ public void writeUnsignedSmart(int value) {
+ if (value < 64 && value >= -64) {
+ write_byte(value + 64);
+ return;
+ }
+ if (value < 16384 && value >= -16384) {
+ writeShort(value + 49152);
+ } else {
+ System.out.println("Error psmart out of range: " + value);
+ }
+ }
+
+
+ /**
+ * Reads a smart words from the buffer.
+ *
+ * @return the read smart value.
+ */
+ @SneakyThrows(Exception.class)
+ public int readSmartWords() {
+ int value = 0;
+ int incr;
+ for (incr = readSmart(); incr == 32767; incr = readSmart()) {
+ value += 32767;
+ }
+ value += incr;
+ return value;
+ }
+
+
+ /**
+ * Writes a smart words to the buffer.
+ *
+ * @param value the smart value to write.
+ */
+ @SneakyThrows(Exception.class)
+ public void writeSmartWords(int value) {
+ while (value > 32767) {
+ writeSmart(value);
+ value -= 32767;
+ }
+ if (value > 0) {
+ writeSmart(value);
+ }
+ }
+
+
+ /**
+ * Reads a big smart value from the buffer.
+ *
+ * @return the read smart value.
+ */
+ @SneakyThrows(Exception.class)
+ public int readBigSmart() {
+ if (payload[pos] < 0) {
+ return getInt() & 0x7fffffff;
+ }
+ return getUnsignedShort();
+ }
+
+
+ /**
+ * Reads a big smart value from the buffer (termination value
+ * -1
supported).
+ *
+ * @return the read smart value.
+ */
+ @SneakyThrows(Exception.class)
+ public int readBigSmartNS() {
+ if (payload[pos] < 0) {
+ return getInt() & 0x7fffffff;
+ }
+ int value = getUnsignedShort();
+ if (value == 32767) {
+ return -1;
+ }
+ return value;
+ }
+
+
+ /**
+ * Writes a big smart value to the buffer.
+ *
+ * @param value the value to write.
+ */
+ @SneakyThrows(Exception.class)
+ public void writeBigSmart(int value) {
+ if (value > 32767) {
+ writeInt(value - Integer.MAX_VALUE - 1);
+ } else {
+ writeShort(value >= 0 ? value : 32767);
+ }
+ }
+
+ @SneakyThrows(Exception.class)
+ public int get_smart_b() {
+ int baseVal = 0;
+ int lastVal = 0;
+ while ((lastVal = getUnsignedSmart()) == 32767) {
+ baseVal += 32767;
+ }
+ return baseVal + lastVal;
+ }
+
+ @SneakyThrows(Exception.class)
+ public String getNewString() {
+ int i = this.pos;
+ while (this.payload[this.pos++] != 0)
+ ;
+ return new String(this.payload, i, pos - i - 1);
+ }
+
+ @SneakyThrows(Exception.class)
+ public void write_byte(int value) {
+ this.payload[this.pos++] = (byte) value;
+ }
+
+ @SneakyThrows(Exception.class)
+ public void writeBoolean(boolean value) {
+ this.payload[this.pos++] = (byte) (value ? 1 : 0);
+ }
+
+ @SneakyThrows(Exception.class)
+ public void writeShort(int value) {
+ this.payload[this.pos++] = (byte) (value >> 8);
+ this.payload[this.pos++] = (byte) value;
+ }
+
+ @SneakyThrows(Exception.class)
+ public void writeTriByte(int value) {
+ this.payload[this.pos++] = (byte) (value >> 16);
+ this.payload[this.pos++] = (byte) (value >> 8);
+ this.payload[this.pos++] = (byte) value;
+ }
+
+ @SneakyThrows(Exception.class)
+ public void writeInt(int value) {
+ this.payload[this.pos++] = (byte) (value >> 24);
+ this.payload[this.pos++] = (byte) (value >> 16);
+ this.payload[this.pos++] = (byte) (value >> 8);
+ this.payload[this.pos++] = (byte) value;
+ }
+
+ @SneakyThrows(Exception.class)
+ public void writeLEInt(int value) {
+ this.payload[this.pos++] = (byte) value;
+ this.payload[this.pos++] = (byte) (value >> 8);
+ this.payload[this.pos++] = (byte) (value >> 16);
+ this.payload[this.pos++] = (byte) (value >> 24);
+ }
+
+ @SneakyThrows(Exception.class)
+ public void writeString(String text) {
+ System.arraycopy(text.getBytes(), 0, this.payload, this.pos, text.length());
+ this.pos += text.length();
+ this.payload[this.pos++] = 10;
+ }
+
+ @SneakyThrows(Exception.class)
+ public void put_bytes(byte data[], int offset, int length) {
+ for (int index = length; index < length + offset; index++)
+ this.payload[this.pos++] = data[index];
+ }
+
+ @SneakyThrows(Exception.class)
+ public void put_length(int length) {
+ this.payload[this.pos - length - 1] = (byte) length;
+ }
+
+ @SneakyThrows(Exception.class)
+ public int getUnsignedByte() {
+ return this.payload[this.pos++] & 0xff;
+ }
+
+ @SneakyThrows(Exception.class)
+ public byte getSignedByte() {
+ return this.payload[this.pos++];
+ }
+
+ @SneakyThrows(Exception.class)
+ public int getUnsignedShort() {
+ this.pos += 2;
+ return ((this.payload[this.pos - 2] & 0xff) << 8) + (this.payload[this.pos - 1] & 0xff);
+ }
+
+ @SneakyThrows(Exception.class)
+ public int getSignedShort() {
+ this.pos += 2;
+ int value = ((this.payload[this.pos - 2] & 0xff) << 8) + (this.payload[this.pos - 1] & 0xff);
+ if (value > 32767)
+ value -= 0x10000;
+
+ return value;
+ }
+
+ @SneakyThrows(Exception.class)
+ public int getShort() {
+ this.pos += 2;
+ int value = ((this.payload[this.pos - 2] & 0xff) << 8) + (this.payload[this.pos - 1] & 0xff);
+
+ if (value > 60000)
+ value = -65535 + value;
+ return value;
+ }
+
+ @SneakyThrows(Exception.class)
+ public int get24BitInt() {
+ this.pos += 3;
+ return ((this.payload[this.pos - 3] & 0xff) << 16) + ((this.payload[this.pos - 2] & 0xff) << 8) + (this.payload[this.pos - 1] & 0xff);
+ }
+
+ @SneakyThrows(Exception.class)
+ public int getInt() {
+ this.pos += 4;
+ return ((this.payload[this.pos - 4] & 0xff) << 24) + ((this.payload[this.pos - 3] & 0xff) << 16) + ((this.payload[this.pos - 2] & 0xff) << 8) + (this.payload[this.pos - 1] & 0xff);
+ }
+
+ @SneakyThrows(Exception.class)
+ public long getLong() {
+ long msi = (long) this.getInt() & 0xffffffffL;
+ long lsi = (long) this.getInt() & 0xffffffffL;
+ return (msi << 32) + lsi;
+ }
+
+ @SneakyThrows(Exception.class)
+ public String getString() {
+
+ int index = this.pos;
+ while (this.payload[this.pos++] != 10)
+ ;
+ return new String(this.payload, index, this.pos - index - 1);
+ }
+
+ private static final char[] CHARACTERS = new char[]{
+ '\u20ac', '\u0000', '\u201a', '\u0192', '\u201e', '\u2026',
+ '\u2020', '\u2021', '\u02c6', '\u2030', '\u0160', '\u2039',
+ '\u0152', '\u0000', '\u017d', '\u0000', '\u0000', '\u2018',
+ '\u2019', '\u201c', '\u201d', '\u2022', '\u2013', '\u2014',
+ '\u02dc', '\u2122', '\u0161', '\u203a', '\u0153', '\u0000',
+ '\u017e', '\u0178'
+ };
+
+ @SneakyThrows(Exception.class)
+ public String getStringOSRS() {
+ StringBuilder sb = new StringBuilder();
+
+ for (; ; ) {
+ int ch = this.getUnsignedByte();
+
+ if (ch == 0) {
+ break;
+ }
+
+ if (ch >= 128 && ch < 160) {
+ char var7 = CHARACTERS[ch - 128];
+ if (0 == var7) {
+ var7 = '?';
+ }
+
+ ch = var7;
+ }
+
+ sb.append((char) ch);
+ }
+ return sb.toString();
+ }
+
+ @SneakyThrows(Exception.class)
+ public byte[] getStringBytes() {
+ int index = this.pos;
+ while (this.payload[this.pos++] != 10)
+ ;
+ byte[] data = new byte[this.pos - index - 1];
+ System.arraycopy(this.payload, index, data, 0, this.pos - 1 - index);
+ return data;
+ }
+
+ @SneakyThrows(Exception.class)
+ public void getBytes(int offset, int length, byte[] data) {
+ for (int index = length; index < length + offset; index++)
+ data[index] = this.payload[this.pos++];
+ }
+
+ @SneakyThrows(Exception.class)
+ public void initBitAccess() {
+ this.bit_pos = this.pos * 8;
+ }
+
+ @SneakyThrows(Exception.class)
+ public int getBits(int amount) {
+ int byte_offset = this.bit_pos >> 3;
+ int bit_offset = 8 - (this.bit_pos & 7);
+ int value = 0;
+ this.bit_pos += amount;
+ for (; amount > bit_offset; bit_offset = 8) {
+ value += (this.payload[byte_offset++] & BIT_MASKS[bit_offset]) << amount - bit_offset;
+ amount -= bit_offset;
+ }
+ if (amount == bit_offset)
+ value += this.payload[byte_offset] & BIT_MASKS[bit_offset];
+ else
+ value += this.payload[byte_offset] >> bit_offset - amount & BIT_MASKS[amount];
+
+ return value;
+ }
+
+ @SneakyThrows(Exception.class)
+ public void finishBitAccess() {
+ this.pos = (this.bit_pos + 7) / 8;
+ }
+
+ @SneakyThrows(Exception.class)
+ public int getSignedSmart() {
+ int value = this.payload[this.pos] & 0xff;
+ if (value < 128) {
+ return this.getUnsignedByte() - 64;
+ } else {
+ return this.getUnsignedShort() - 49152;
+ }
+ }
+
+ @SneakyThrows(Exception.class)
+ public int getUnsignedSmart() {
+ int value = this.payload[this.pos] & 0xff;
+ if (value < 128) {
+ return this.getUnsignedByte();
+ } else {
+ return this.getUnsignedShort() - 32768;
+ }
+ }
+
+ @SneakyThrows(Exception.class)
+ public void putAddedByte(int value) {
+ this.payload[this.pos++] = (byte) (value + 128);
+ }
+
+ public void putNegatedByte(int value) {
+ this.payload[this.pos++] = (byte) (-value);
+ }
+
+ public void putSubtractedByte(int value) {
+ this.payload[this.pos++] = (byte) (128 - value);
+ }
+
+ public int getAddedByte() {
+ return this.payload[this.pos++] - 128 & 0xff;
+ }
+
+ public int getNegatedByte() {
+ return -this.payload[this.pos++] & 0xff;
+ }
+
+ public int getSubtractedByte() {
+ return 128 - this.payload[this.pos++] & 0xff;
+ }
+
+ public byte getSignedAddedByte() {
+ return (byte) (this.payload[this.pos++] - 128);
+ }
+
+ public byte getSignedNegatedByte() {
+ return (byte) -this.payload[this.pos++];
+ }
+
+ public byte getSignedSubtractedByte() {
+ return (byte) (128 - this.payload[this.pos++]);
+ }
+
+ public void putLEShortDuplicate(int value) {
+ this.payload[this.pos++] = (byte) value;
+ this.payload[this.pos++] = (byte) (value >> 8);
+ }
+
+ public void putShortAdded(int value) {
+ this.payload[this.pos++] = (byte) (value >> 8);
+ this.payload[this.pos++] = (byte) (value + 128);
+ }
+
+ public void putLEShortAdded(int value) {
+ this.payload[this.pos++] = (byte) (value + 128);
+ this.payload[this.pos++] = (byte) (value >> 8);
+ }
+
+ public int method549() {//TODO
+ this.pos += 2;
+ return ((this.payload[this.pos - 1] & 0xff) << 8) + (this.payload[this.pos - 2] & 0xff);
+ }
+
+ public int method550() {//TODO
+ this.pos += 2;
+ return ((this.payload[this.pos - 2] & 0xff) << 8) + (this.payload[this.pos - 1] - 128 & 0xff);
+ }
+
+ public int get_little_short() {
+ this.pos += 2;
+ return ((this.payload[this.pos - 1] & 0xff) << 8) + (this.payload[this.pos - 2] - 128 & 0xff);
+ }
+
+ public int method552() {//TODO
+ this.pos += 2;
+ int value = ((this.payload[this.pos - 1] & 0xff) << 8) + (this.payload[this.pos - 2] & 0xff);
+
+ if (value > 32767)
+ value -= 0x10000;
+ return value;
+ }
+
+ public int method553() {//TODO
+ this.pos += 2;
+ int value = ((this.payload[this.pos - 1] & 0xff) << 8) + (this.payload[this.pos - 2] - 128 & 0xff);
+ if (value > 32767)
+ value -= 0x10000;
+
+ return value;
+ }
+
+ public int method555() {//TODO
+ this.pos += 4;
+ return ((this.payload[this.pos - 2] & 0xff) << 24)
+ + ((this.payload[this.pos - 1] & 0xff) << 16)
+ + ((this.payload[this.pos - 4] & 0xff) << 8)
+ + (this.payload[this.pos - 3] & 0xff);
+ }
+
+ public int method556() {//TODO
+ this.pos += 4;
+ return ((this.payload[this.pos - 3] & 0xff) << 24)
+ + ((this.payload[this.pos - 4] & 0xff) << 16)
+ + ((this.payload[this.pos - 1] & 0xff) << 8)
+ + (this.payload[this.pos - 2] & 0xff);
+ }
+
+ public int method557() {//TODO
+ this.pos += 4;
+ return ((this.payload[this.pos - 2] & 255) << 8)
+ + ((this.payload[this.pos - 4] & 255) << 24)
+ + ((this.payload[this.pos - 3] & 255) << 16)
+ + (this.payload[this.pos - 1] & 255);
+ }
+
+
+ public void putReverseData(byte[] data, int length, int offset) {
+ for (int index = (length + offset) - 1; index >= length; index--)
+ this.payload[this.pos++] = (byte) (data[index] + 128);
+
+ }
+
+ public void getReverseData(byte[] data, int offset, int length) {
+ for (int index = (length + offset) - 1; index >= length; index--)
+ data[index] = this.payload[this.pos++];
+
+ }
+
+ public byte[] payload;
+ public int pos;
+
+ public int bit_pos;
+ private static final int[] BIT_MASKS = {
+ 0, 1, 3, 7, 15, 31, 63, 127, 255,
+ 511, 1023, 2047, 4095, 8191, 16383, 32767, 65535, 0x1ffff, 0x3ffff,
+ 0x7ffff, 0xfffff, 0x1fffff, 0x3fffff, 0x7fffff, 0xffffff,
+ 0x1ffffff, 0x3ffffff, 0x7ffffff, 0xfffffff, 0x1fffffff, 0x3fffffff,
+ 0x7fffffff, -1
+ };
+
+ private static int cache_indices;
+ private static final LinkedList BUFFER_CACHE = new LinkedList();
+}
diff --git a/src/java/modelviewer/scene/RSMeshGroup.java b/src/java/modelviewer/scene/RSMeshGroup.java
new file mode 100644
index 0000000..500bb22
--- /dev/null
+++ b/src/java/modelviewer/scene/RSMeshGroup.java
@@ -0,0 +1,130 @@
+package modelviewer.scene;
+
+import com.application.AppConstants;
+import com.application.GUI;
+import javafx.scene.image.Image;
+import javafx.scene.paint.PhongMaterial;
+import javafx.scene.shape.MeshView;
+import javafx.scene.shape.TriangleMesh;
+import javafx.scene.transform.Scale;
+import lombok.Getter;
+import modelviewer.model.Vector3i;
+import modelviewer.util.ColorUtils;
+import net.runelite.cache.TextureManager;
+import net.runelite.cache.definitions.ModelDefinition;
+import net.runelite.cache.definitions.SpriteDefinition;
+import net.runelite.cache.definitions.loaders.SpriteLoader;
+import net.runelite.cache.fs.Store;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+@Getter
+public class RSMeshGroup {
+ private final ModelDefinition model;
+ private final List meshes = new ArrayList<>();
+ public final float MODEL_SCALE = 0.03f;
+
+ private TextureManager textureManager;
+
+ public RSMeshGroup(ModelDefinition model) {
+ this.model = model;
+ }
+
+ private Image texture;
+
+ public void buildMeshes() throws IOException {
+ if (AppConstants.cacheType.equals("Old School RuneScape")) {
+ Store store = new Store(new File(GUI.cacheLibrary.getPath()));
+ store.load();
+ textureManager = new TextureManager(store);
+ textureManager.load();
+ }
+ model.computeTextureUVCoordinates();
+ for (int face = 0; face < model.faceCount; face++) {
+ TriangleMesh mesh = new TriangleMesh();
+ int faceA = model.faceIndices1[face];
+ int faceB = model.faceIndices2[face];
+ int faceC = model.faceIndices3[face];
+
+ Vector3i v1 = new Vector3i(model.vertexX[faceA], model.vertexY[faceA], model.vertexZ[faceA]);
+ Vector3i v2 = new Vector3i(model.vertexX[faceB], model.vertexY[faceB], model.vertexZ[faceB]);
+ Vector3i v3 = new Vector3i(model.vertexX[faceC], model.vertexY[faceC], model.vertexZ[faceC]);
+
+ mesh.getPoints()
+ .addAll(v1.x(), v1.y(), v1.z(), v2.x(), v2.y(), v2.z(), v3.x(), v3.y(), v3.z());
+
+ mesh.getFaces().addAll(
+ 0, 0, 1, 1, 2, 2
+ );
+ boolean textured = model.faceTextures != null;
+ if (textured) {
+ mesh.getTexCoords()
+ .addAll(model.faceTextureUCoordinates[face][0], model.faceTextureVCoordinates[face][0]);
+ mesh.getTexCoords()
+ .addAll(model.faceTextureUCoordinates[face][1], model.faceTextureVCoordinates[face][1]);
+ mesh.getTexCoords()
+ .addAll(model.faceTextureUCoordinates[face][2], model.faceTextureVCoordinates[face][2]);
+ } else {
+ mesh.getTexCoords().addAll(0f, 0f, 1f, 0f, 0f, 1f);
+ }
+ MeshView view = new MeshView(mesh);
+ view.getTransforms().add(new Scale(MODEL_SCALE, MODEL_SCALE, MODEL_SCALE));
+ if (textured) {
+ PhongMaterial mat = new PhongMaterial();
+ if (model.faceTextures != null && model.faceTextures[face] != -1 && AppConstants.cacheType.equals("Old School RuneScape")) {
+ texture = exportToImage(textureManager.findTexture(model.faceTextures[face]).getFileIds()[0]);
+ mat.setDiffuseMap(texture);
+ view.setMaterial(mat);
+ }
+ else {
+ view.setMaterial(new PhongMaterial(ColorUtils.rs2HSLToColor(model.faceColors[face], model.faceTransparencies == null ? 0 : model.faceTransparencies[face])));
+ }
+ } else {
+ view.setMaterial(new PhongMaterial(ColorUtils.rs2HSLToColor(model.faceColors[face], model.faceTransparencies == null ? 0 : model.faceTransparencies[face])));
+ }
+ initListeners(view);
+ meshes.add(view);
+ }
+ }
+
+ private void initListeners(MeshView view) {
+ view.setOnMouseClicked(event -> {
+ paint(view);
+ });
+ view.setOnMouseEntered(event -> {
+ if (event.isAltDown()) {
+ paint(view);
+ }
+ });
+ }
+
+ private void paint(MeshView view) {
+ PhongMaterial mat = new PhongMaterial();
+ mat.setDiffuseMap(texture);
+ view.setMaterial(mat);
+ }
+
+ public BufferedImage export(SpriteDefinition spriteDefinition)
+ {
+ BufferedImage bi = new BufferedImage(spriteDefinition.getWidth(), spriteDefinition.getHeight(), BufferedImage.TYPE_INT_ARGB);
+ bi.setRGB(0, 0, spriteDefinition.getWidth(), spriteDefinition.getHeight(), spriteDefinition.getPixels(), 0, spriteDefinition.getWidth());
+ return bi;
+ }
+
+ public Image exportToImage(int spriteID) throws IOException
+ {
+ SpriteLoader spriteLoader = new SpriteLoader();
+ SpriteDefinition spriteDefinition = spriteLoader.load(spriteID, GUI.cacheLibrary.data(8, spriteID, 0))[0];
+ BufferedImage image = export(spriteDefinition);
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ ImageIO.write(image, "png", byteArrayOutputStream);
+ return new Image(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
+ }
+}
diff --git a/src/java/modelviewer/scene/camera/OrbitCamera.java b/src/java/modelviewer/scene/camera/OrbitCamera.java
new file mode 100644
index 0000000..e024768
--- /dev/null
+++ b/src/java/modelviewer/scene/camera/OrbitCamera.java
@@ -0,0 +1,119 @@
+package modelviewer.scene.camera;
+
+import javafx.scene.Camera;
+import javafx.scene.Group;
+import javafx.scene.PerspectiveCamera;
+import javafx.scene.SubScene;
+import javafx.scene.transform.Rotate;
+import javafx.scene.transform.Translate;
+import modelviewer.model.Xform;
+
+public class OrbitCamera {
+
+ private final SubScene subScene;
+ private final Group root3D;
+
+ private final double MAX_ZOOM = 300.0;
+
+ public OrbitCamera(SubScene subScene, Group root) {
+ this.subScene = subScene;
+ this.root3D = root;
+ init();
+ }
+
+ private void init() {
+ camera.setNearClip(0.1D);
+ camera.setFarClip(MAX_ZOOM * 1.15D);
+ camera.getTransforms().addAll(
+ yUpRotate,
+ cameraPosition,
+ cameraLookXRotate,
+ cameraLookZRotate
+ );
+
+ Group rotateGroup = new Group();
+ rotateGroup.getChildren().addAll(cameraXform);
+ cameraXform.ry.setAngle(180);
+ cameraXform.rx.setAngle(-18);
+ cameraXform.getChildren().add(cameraXform2);
+ cameraXform2.getChildren().add(cameraXform3);
+ cameraXform3.getChildren().add(camera);
+ cameraPosition.setZ(-cameraDistance);
+
+ root3D.getChildren().addAll(rotateGroup);
+
+ subScene.setCamera(camera);
+ subScene.setOnScroll(event -> {
+
+ double zoomFactor = 1.05;
+ double deltaY = event.getDeltaY();
+
+ if (deltaY < 0) {
+ zoomFactor = 2.0 - zoomFactor;
+ }
+ double z = cameraPosition.getZ() / zoomFactor;
+ z = Math.max(z, -MAX_ZOOM);
+ z = Math.min(z, 10.0);
+ cameraPosition.setZ(z);
+ });
+
+ subScene.setOnMousePressed(event -> {
+ if (!event.isAltDown()) {
+ mousePosX = event.getSceneX();
+ mousePosY = event.getSceneY();
+ mouseOldX = event.getSceneX();
+ mouseOldY = event.getSceneY();
+ }
+ });
+
+ subScene.setOnMouseDragged(event -> {
+ if (!event.isAltDown()) {
+ double modifier = 1.0;
+ double modifierFactor = 0.3;
+
+ if (event.isControlDown()) modifier = 0.1;
+ if (event.isSecondaryButtonDown()) modifier = 0.035;
+
+ mouseOldX = mousePosX;
+ mouseOldY = mousePosY;
+ mousePosX = event.getSceneX();
+ mousePosY = event.getSceneY();
+ mouseDeltaX = mousePosX - mouseOldX;
+ mouseDeltaY = mousePosY - mouseOldY;
+
+ double flip = -1.0;
+
+ if (event.isSecondaryButtonDown()) {
+ double newX = cameraXform2.t.getX() + flip * mouseDeltaX * modifierFactor * modifier * 2.0;
+ double newY = cameraXform2.t.getY() + 1.0 * -mouseDeltaY * modifierFactor * modifier * 2.0;
+ cameraXform2.t.setX(newX);
+ cameraXform2.t.setY(newY);
+ } else if (event.isPrimaryButtonDown()) {
+ double yAngle = cameraXform.ry.getAngle() - 1.0 * -mouseDeltaX * modifierFactor * modifier * 2.0;
+ double xAngle = cameraXform.rx.getAngle() + flip * mouseDeltaY * modifierFactor * modifier * 2.0;
+ cameraXform.ry.setAngle(yAngle);
+ cameraXform.rx.setAngle(xAngle);
+ }
+ }
+ });
+ }
+
+
+ private final Camera camera = new PerspectiveCamera(true);
+ private final Rotate cameraXRotate = new Rotate(-20.0, 0.0, 0.0, 0.0, Rotate.X_AXIS);
+ private final Rotate cameraYRotate = new Rotate(-20.0, 0.0, 0.0, 0.0, Rotate.Y_AXIS);
+ private final Rotate cameraLookXRotate = new Rotate(0.0, 0.0, 0.0, 0.0, Rotate.X_AXIS);
+ private final Rotate cameraLookZRotate = new Rotate(0.0, 0.0, 0.0, 0.0, Rotate.Z_AXIS);
+ private final Translate cameraPosition = new Translate(0.0, 0.0, 0.0);
+ private Xform cameraXform = new Xform();
+ private Xform cameraXform2 = new Xform();
+ private Xform cameraXform3 = new Xform();
+ private double cameraDistance = 25.0;
+ private double mousePosX = 0;
+ private double mousePosY = 0;
+ private double mouseOldX = 0;
+ private double mouseOldY = 0;
+ private double mouseDeltaX = 0;
+ private double mouseDeltaY = 0;
+ private final Rotate yUpRotate = new Rotate(0.0, 0.0, 0.0, 0.0, Rotate.X_AXIS);
+}
diff --git a/src/java/modelviewer/util/ColorUtils.java b/src/java/modelviewer/util/ColorUtils.java
new file mode 100644
index 0000000..b0a39d6
--- /dev/null
+++ b/src/java/modelviewer/util/ColorUtils.java
@@ -0,0 +1,22 @@
+package modelviewer.util;
+
+import javafx.scene.paint.Color;
+
+public final class ColorUtils {
+
+ public static Color rs2HSLToColor(short hsl, int alpha) {
+ int transparency = alpha;
+ if (transparency <= 0) {
+ transparency = 255;
+ }
+
+ int hue = hsl >> 10 & 0x3f;
+ int sat = hsl >> 7 & 0x07;
+ int bri = hsl & 0x7f;
+ java.awt.Color awtCol = java.awt.Color.getHSBColor((float) hue / 63, (float) sat / 7, (float) bri / 127);
+ double r = awtCol.getRed() / 255.0;
+ double g = awtCol.getGreen() / 255.0;
+ double b = awtCol.getBlue() / 255.0;
+ return Color.color(r, g, b, transparency / 255.0);
+ }
+}
diff --git a/src/java/net/runelite/cache/TextureManager.java b/src/java/net/runelite/cache/TextureManager.java
index 0b01d47..eb6747b 100644
--- a/src/java/net/runelite/cache/TextureManager.java
+++ b/src/java/net/runelite/cache/TextureManager.java
@@ -35,8 +35,8 @@
public class TextureManager implements TextureProvider
{
- private final Store store;
- private final List textures = new ArrayList<>();
+ public final Store store;
+ public final List textures = new ArrayList<>();
public TextureManager(Store store)
{
diff --git a/src/java/net/runelite/cache/definitions/TextureDefinition.java b/src/java/net/runelite/cache/definitions/TextureDefinition.java
index 820d73b..d0c810b 100644
--- a/src/java/net/runelite/cache/definitions/TextureDefinition.java
+++ b/src/java/net/runelite/cache/definitions/TextureDefinition.java
@@ -42,9 +42,11 @@ public class TextureDefinition
public int animationDirection;
public transient int[] pixels;
+ public int width;
public boolean method2680(double var1, int var3, SpriteProvider spriteProvider)
{
+ width = var3;
int var5 = var3 * var3;
this.pixels = new int[var5];
diff --git a/src/java/rshd/ModelData.java b/src/java/rshd/ModelData.java
index 4aea87c..f4249d9 100644
--- a/src/java/rshd/ModelData.java
+++ b/src/java/rshd/ModelData.java
@@ -54,7 +54,6 @@ public ModelDefinition load(int modelId, byte[] data) {
if (data[data.length - 1] == -1 && data[data.length - 2] == -1) {
decodeNewFormat(modelDefinition, data);
- modelDefinition.resize(32, 32, 32);
modelDefinition.setModelData(data);
}
/*
diff --git a/src/resources/META-INF/MANIFEST.MF b/src/resources/META-INF/MANIFEST.MF
index 42d8598..797d99b 100644
--- a/src/resources/META-INF/MANIFEST.MF
+++ b/src/resources/META-INF/MANIFEST.MF
@@ -1,3 +1,3 @@
Manifest-Version: 1.0
-Main-Class: application.Main
+Main-Class: com.application.Main
diff --git a/src/resources/css/darktheme.css b/src/resources/css/darktheme.css
new file mode 100644
index 0000000..7826860
--- /dev/null
+++ b/src/resources/css/darktheme.css
@@ -0,0 +1,60 @@
+/*
+ * This is an adjustment of the original modena.css for a consistent dark theme.
+ * Original modena.css here: https://gist.github.com/maxd/63691840fc372f22f470.
+ */
+
+/* Redefine base colors */
+.root {
+ -fx-base: rgb(50, 50, 50);
+ -fx-background: rgb(50, 50, 50);
+
+ /* make controls (buttons, thumb, etc.) slightly lighter */
+ -fx-color: derive(-fx-base, 10%);
+
+ /* text fields and table rows background */
+ -fx-control-inner-background: rgb(20, 20, 20);
+ /* version of -fx-control-inner-background for alternative rows */
+ -fx-control-inner-background-alt: derive(-fx-control-inner-background, 2.5%);
+
+ /* text colors depending on background's brightness */
+ -fx-light-text-color: rgb(220, 220, 220);
+ -fx-mid-text-color: rgb(100, 100, 100);
+ -fx-dark-text-color: rgb(20, 20, 20);
+
+ /* A bright blue for highlighting/accenting objects. For example: selected
+ * text; selected items in menus, lists, trees, and tables; progress bars */
+ -fx-accent: rgb(0, 80, 100);
+
+ /* color of non-focused yet selected elements */
+ -fx-selection-bar-non-focused: rgb(50, 50, 50);
+}
+
+/* Fix derived prompt color for text fields */
+.text-input {
+ -fx-prompt-text-fill: derive(-fx-control-inner-background, +50%);
+}
+
+
+/* Keep prompt invisible when focused (above color fix overrides it) */
+.text-input:focused {
+ -fx-prompt-text-fill: transparent;
+}
+
+
+
+/* Fix scroll bar buttons arrows colors */
+.scroll-bar > .increment-button > .increment-arrow,
+.scroll-bar > .decrement-button > .decrement-arrow {
+ -fx-background-color: -fx-mark-highlight-color, rgb(220, 220, 220);
+}
+
+.scroll-bar > .increment-button:hover > .increment-arrow,
+.scroll-bar > .decrement-button:hover > .decrement-arrow {
+ -fx-background-color: -fx-mark-highlight-color, rgb(240, 240, 240);
+}
+
+
+.scroll-bar > .increment-button:pressed > .increment-arrow,
+.scroll-bar > .decrement-button:pressed > .decrement-arrow {
+ -fx-background-color: -fx-mark-highlight-color, rgb(255, 255, 255);
+}
\ No newline at end of file
diff --git a/src/resources/fxml/ui.fxml b/src/resources/fxml/ui.fxml
new file mode 100644
index 0000000..0ea559f
--- /dev/null
+++ b/src/resources/fxml/ui.fxml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+