From 9ff1b86d830587062267fc969daba027a8d67532 Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Wed, 6 Aug 2025 13:49:42 +0300 Subject: [PATCH 01/33] Add GLTF export option --- craft3data/src/com/hiveworkshop/gltf/GLTFExport.java | 11 +++++++++++ matrixeater/src/com/matrixeater/src/MainPanel.java | 5 +++++ 2 files changed, 16 insertions(+) create mode 100644 craft3data/src/com/hiveworkshop/gltf/GLTFExport.java diff --git a/craft3data/src/com/hiveworkshop/gltf/GLTFExport.java b/craft3data/src/com/hiveworkshop/gltf/GLTFExport.java new file mode 100644 index 00000000..85c6f988 --- /dev/null +++ b/craft3data/src/com/hiveworkshop/gltf/GLTFExport.java @@ -0,0 +1,11 @@ +package com.hiveworkshop.gltf; + +import java.awt.event.ActionListener; +import java.awt.event.ActionEvent; + +public class GLTFExport implements ActionListener{ + @Override + public void actionPerformed(ActionEvent e) { + System.out.println("GLTF Export action performed"); + } +} diff --git a/matrixeater/src/com/matrixeater/src/MainPanel.java b/matrixeater/src/com/matrixeater/src/MainPanel.java index 258ef899..7688971d 100644 --- a/matrixeater/src/com/matrixeater/src/MainPanel.java +++ b/matrixeater/src/com/matrixeater/src/MainPanel.java @@ -107,6 +107,7 @@ import org.lwjgl.util.vector.Vector3f; import org.lwjgl.util.vector.Vector4f; +import com.hiveworkshop.gltf.GLTFExport; import com.hiveworkshop.wc3.gui.BLPHandler; import com.hiveworkshop.wc3.gui.ExceptionPopup; import com.hiveworkshop.wc3.gui.GUIUtils; @@ -3713,6 +3714,10 @@ public void selectVertices(final Collection vertices) { }); scriptsMenu.add(deleteLODs); + final JMenuItem gltfExport = new JMenuItem("GLTF Export"); + gltfExport.addActionListener(new GLTFExport()); + scriptsMenu.add(gltfExport); + final JMenuItem jokebutton = new JMenuItem("Load Retera Land"); jokebutton.setMnemonic(KeyEvent.VK_A); jokebutton.addActionListener(new ActionListener() { From 551dc8e8a03bb61d35424493ae3e4fe98b7bb2e5 Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Wed, 6 Aug 2025 17:26:12 +0300 Subject: [PATCH 02/33] add get unit paths --- .../src/com/hiveworkshop/gltf/GLTFExport.java | 11 ---- .../src/com/matrixeater/gltf/GLTFExport.java | 58 +++++++++++++++++++ .../src/com/matrixeater/src/MainPanel.java | 4 +- 3 files changed, 60 insertions(+), 13 deletions(-) delete mode 100644 craft3data/src/com/hiveworkshop/gltf/GLTFExport.java create mode 100644 matrixeater/src/com/matrixeater/gltf/GLTFExport.java diff --git a/craft3data/src/com/hiveworkshop/gltf/GLTFExport.java b/craft3data/src/com/hiveworkshop/gltf/GLTFExport.java deleted file mode 100644 index 85c6f988..00000000 --- a/craft3data/src/com/hiveworkshop/gltf/GLTFExport.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.hiveworkshop.gltf; - -import java.awt.event.ActionListener; -import java.awt.event.ActionEvent; - -public class GLTFExport implements ActionListener{ - @Override - public void actionPerformed(ActionEvent e) { - System.out.println("GLTF Export action performed"); - } -} diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java new file mode 100644 index 00000000..52990dab --- /dev/null +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -0,0 +1,58 @@ +package com.matrixeater.gltf; + +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +import com.hiveworkshop.wc3.jworldedit.objects.UnitEditorPanel; +import com.hiveworkshop.wc3.mdl.EditableModel; +import com.hiveworkshop.wc3.units.objectdata.MutableObjectData; +import com.hiveworkshop.wc3.units.objectdata.War3ID; +import com.matrixeater.src.MainPanel; + +import java.awt.event.ActionEvent; + +public class GLTFExport implements ActionListener{ + private final Logger log = Logger.getLogger(GLTFExport.class.getName()); + // I know it's bad, don't worry. + private final MainPanel mainframe; + + public GLTFExport(MainPanel mainframe) { + this.mainframe = mainframe; + } + + @Override + public void actionPerformed(ActionEvent e) { + // var model = mainframe.currentMDL(); + // if (model != null) { + // // Process the model and export it to GLTF format + // this.processModel(model); + // } else { + // log.warning("No model is currently loaded for GLTF export."); + // } + log.info(this.getAllUnitPaths().size() + " unit paths found for GLTF export."); + } + + private List getAllUnitPaths() { + List unitPaths = new ArrayList(); + var unitData = mainframe.getUnitData(); + log.info("Unit data: " + unitData.keySet().size()); + final War3ID modelFileId = War3ID.fromString("umdl"); + for (var id : unitData.keySet()) + { + var unit = unitData.get(id); + String modelPath = unit.getFieldAsString(modelFileId, 0); + if (!modelPath.isEmpty()) { + unitPaths.add(modelPath); + } + } + return unitPaths; + } + + private void processModel(EditableModel model) { + // Implement the logic to process the model and export it to GLTF format + log.info("Processing model for GLTF export: " + model.getName()); + // Add your GLTF export logic here + } +} diff --git a/matrixeater/src/com/matrixeater/src/MainPanel.java b/matrixeater/src/com/matrixeater/src/MainPanel.java index 7688971d..cce3d331 100644 --- a/matrixeater/src/com/matrixeater/src/MainPanel.java +++ b/matrixeater/src/com/matrixeater/src/MainPanel.java @@ -107,7 +107,6 @@ import org.lwjgl.util.vector.Vector3f; import org.lwjgl.util.vector.Vector4f; -import com.hiveworkshop.gltf.GLTFExport; import com.hiveworkshop.wc3.gui.BLPHandler; import com.hiveworkshop.wc3.gui.ExceptionPopup; import com.hiveworkshop.wc3.gui.GUIUtils; @@ -230,6 +229,7 @@ import com.hiveworkshop.wc3.util.IconUtils; import com.hiveworkshop.wc3.util.ModelUtils; import com.hiveworkshop.wc3.util.ModelUtils.Mesh; +import com.matrixeater.gltf.GLTFExport; import com.matrixeater.imp.AnimationTransfer; import com.matrixeaterhayate.TextureManager; import com.owens.oobjloader.builder.Build; @@ -3715,7 +3715,7 @@ public void selectVertices(final Collection vertices) { scriptsMenu.add(deleteLODs); final JMenuItem gltfExport = new JMenuItem("GLTF Export"); - gltfExport.addActionListener(new GLTFExport()); + gltfExport.addActionListener(new GLTFExport(this)); scriptsMenu.add(gltfExport); final JMenuItem jokebutton = new JMenuItem("Load Retera Land"); From 8adb2f029bed2dfb946fddd70d84e2b0c07677fd Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Wed, 6 Aug 2025 18:03:13 +0300 Subject: [PATCH 03/33] Add loadModel --- .../src/com/matrixeater/gltf/GLTFExport.java | 67 ++++++++++++++++--- 1 file changed, 59 insertions(+), 8 deletions(-) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index 52990dab..f86b0858 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -7,13 +7,17 @@ import com.hiveworkshop.wc3.jworldedit.objects.UnitEditorPanel; import com.hiveworkshop.wc3.mdl.EditableModel; +import com.hiveworkshop.wc3.mdx.MdxUtils; +import com.hiveworkshop.wc3.mpq.MpqCodebase; import com.hiveworkshop.wc3.units.objectdata.MutableObjectData; import com.hiveworkshop.wc3.units.objectdata.War3ID; import com.matrixeater.src.MainPanel; +import de.wc3data.stream.BlizzardDataInputStream; + import java.awt.event.ActionEvent; -public class GLTFExport implements ActionListener{ +public class GLTFExport implements ActionListener { private final Logger log = Logger.getLogger(GLTFExport.class.getName()); // I know it's bad, don't worry. private final MainPanel mainframe; @@ -26,23 +30,25 @@ public GLTFExport(MainPanel mainframe) { public void actionPerformed(ActionEvent e) { // var model = mainframe.currentMDL(); // if (model != null) { - // // Process the model and export it to GLTF format - // this.processModel(model); + // // Process the model and export it to GLTF format + // this.processModel(model); // } else { - // log.warning("No model is currently loaded for GLTF export."); + // log.warning("No model is currently loaded for GLTF export."); // } log.info(this.getAllUnitPaths().size() + " unit paths found for GLTF export."); + log.info(this.getAllDoodadsPaths().size() + " doodad paths found for GLTF export."); + var model0 = this.loadModel(this.getAllUnitPaths().get(0)); + log.info("name: " + model0.getName()); } - private List getAllUnitPaths() { + private List getAllUnitPaths() { List unitPaths = new ArrayList(); var unitData = mainframe.getUnitData(); log.info("Unit data: " + unitData.keySet().size()); final War3ID modelFileId = War3ID.fromString("umdl"); - for (var id : unitData.keySet()) - { + for (var id : unitData.keySet()) { var unit = unitData.get(id); - String modelPath = unit.getFieldAsString(modelFileId, 0); + String modelPath = convertPathToMDX(unit.getFieldAsString(modelFileId, 0)); if (!modelPath.isEmpty()) { unitPaths.add(modelPath); } @@ -50,9 +56,54 @@ private List getAllUnitPaths() { return unitPaths; } + private List getAllDoodadsPaths() { + List doodadPaths = new ArrayList(); + var data = mainframe.getDoodadData(); + log.info("Doodad data: " + data.keySet().size()); + final War3ID modelFileId = War3ID.fromString("umdl"); + for (var id : data.keySet()) { + var obj = data.get(id); + final int numberOfVariations = obj.getFieldAsInteger(War3ID.fromString("dvar"), 0); + if (numberOfVariations > 1) { + for (int i = 0; i < numberOfVariations; i++) { + final String path = convertPathToMDX( + obj.getFieldAsString(War3ID.fromString("dfil"), 0) + i + ".mdl"); + doodadPaths.add(path); + } + } else { + final String path = convertPathToMDX(obj.getFieldAsString(War3ID.fromString("dfil"), 0)); + if (!path.isEmpty()) { + doodadPaths.add(path); + } + } + } + return doodadPaths; + } + private void processModel(EditableModel model) { // Implement the logic to process the model and export it to GLTF format log.info("Processing model for GLTF export: " + model.getName()); // Add your GLTF export logic here } + + private EditableModel loadModel(String path) { + var f = MpqCodebase.get().getResourceAsStream(path); + try (BlizzardDataInputStream in = new BlizzardDataInputStream(f)) { + final EditableModel model = new EditableModel(MdxUtils.loadModel(in)); + return model; + } + catch (Exception e) { + log.severe("Failed to load model from path: " + path + " due to " + e.getMessage()); + return null; + } + } + + private String convertPathToMDX(String filepath) { + if (filepath.endsWith(".mdl")) { + filepath = filepath.replace(".mdl", ".mdx"); + } else if (!filepath.endsWith(".mdx")) { + filepath = filepath.concat(".mdx"); + } + return filepath; + } } From 31de2d932210ec4b9ea40f7d2650843acd99d97a Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Wed, 6 Aug 2025 21:59:16 +0300 Subject: [PATCH 04/33] Update Gradle and add placeholder GLTF --- build.gradle | 22 ++-- craft3data/build.gradle | 3 +- craft3editor/build.gradle | 3 +- gradle/wrapper/gradle-wrapper.properties | 2 +- matrixeater/build.gradle | 16 ++- .../src/com/matrixeater/gltf/GLTFExport.java | 109 ++++++++++++++++-- 6 files changed, 125 insertions(+), 30 deletions(-) diff --git a/build.gradle b/build.gradle index e1981a1c..e741f0cb 100644 --- a/build.gradle +++ b/build.gradle @@ -1,14 +1,11 @@ buildscript { repositories { mavenLocal() - flatDir { - dirs "$rootProject.projectDir/jars" - } mavenCentral() + google() maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } maven { url "https://maven.nikr.net/" } gradlePluginPortal() - google() } } @@ -40,13 +37,20 @@ allprojects { maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } maven { url "https://maven.nikr.net/" } maven { url "https://oss.sonatype.org/content/repositories/releases/" } + maven { url "https://www.javagl.de/repo/" } } } -project(":matrixeater") { +subprojects { apply plugin: "java-library" + java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} +project(":matrixeater") { dependencies { implementation project(":craft3data") implementation project(":craft3editor") @@ -56,9 +60,6 @@ project(":matrixeater") { } project(":craft3editor") { - apply plugin: "java-library" - - dependencies { implementation project(":craft3data") api "org.jclarion:image4j:$image4jVersion" @@ -66,9 +67,6 @@ project(":craft3editor") { } project(":craft3data") { - apply plugin: "java-library" - - dependencies { api "com.jtattoo:JTattoo:$jtattooVersion" api "com.miglayout:miglayout-swing:$miglayoutVersion" @@ -79,8 +77,6 @@ project(":craft3data") { api "org.lwjgl.lwjgl:lwjgl_util:${lwjglVersion}" api "net.nikr:dds:1.0.0" api "com.glazedlists:glazedlists:1.11.0" -// compile "com.github.ebourg:infonode:master" -// compile "com.github.DrSuperGood:blp-iio-plugin:master" api files(fileTree(dir:'../jars', includes: ['*.jar'])) } } diff --git a/craft3data/build.gradle b/craft3data/build.gradle index cd4b79da..caac0876 100644 --- a/craft3data/build.gradle +++ b/craft3data/build.gradle @@ -1,7 +1,6 @@ +apply plugin: "java-library" [compileJava, compileTestJava]*.options*.encoding = 'UTF-8' -sourceCompatibility = 1.17 - sourceSets.main.java.srcDirs = [ "src/" ] diff --git a/craft3editor/build.gradle b/craft3editor/build.gradle index f773c8c5..cb244d61 100644 --- a/craft3editor/build.gradle +++ b/craft3editor/build.gradle @@ -1,7 +1,6 @@ +apply plugin: "java-library" [compileJava, compileTestJava]*.options*.encoding = 'UTF-8' -sourceCompatibility = 1.17 - sourceSets.main.java.srcDirs = [ "src/" ] diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e750102e..a5952066 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/matrixeater/build.gradle b/matrixeater/build.gradle index 63f9fed5..59ca1508 100644 --- a/matrixeater/build.gradle +++ b/matrixeater/build.gradle @@ -1,8 +1,22 @@ plugins { id 'org.beryx.runtime' version '1.12.5' + id 'java' } -sourceCompatibility = 1.17 +repositories { + mavenCentral() + maven { url "https://www.javagl.de/repo/" } +} + +dependencies { + implementation project(':craft3data') + implementation fileTree('lib') { + include '*.jar' + } + implementation 'de.javagl:jgltf-model:2.0.3' + implementation 'de.javagl:jgltf-impl-v2:2.0.3' + implementation 'de.javagl:jgltf-obj:2.0.3' +} [compileJava, compileTestJava]*.options*.encoding = 'UTF-8' diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index f86b0858..51d940fe 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -1,21 +1,30 @@ package com.matrixeater.gltf; +import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; -import com.hiveworkshop.wc3.jworldedit.objects.UnitEditorPanel; +import javax.swing.JFileChooser; + import com.hiveworkshop.wc3.mdl.EditableModel; +import com.hiveworkshop.wc3.mdl.Geoset; +import com.hiveworkshop.wc3.mdl.GeosetVertex; +import com.hiveworkshop.wc3.mdl.Triangle; import com.hiveworkshop.wc3.mdx.MdxUtils; -import com.hiveworkshop.wc3.mpq.MpqCodebase; -import com.hiveworkshop.wc3.units.objectdata.MutableObjectData; -import com.hiveworkshop.wc3.units.objectdata.War3ID; -import com.matrixeater.src.MainPanel; +import de.javagl.jgltf.model.GltfModel; +import de.javagl.jgltf.model.io.GltfModelWriter; import de.wc3data.stream.BlizzardDataInputStream; -import java.awt.event.ActionEvent; +import com.hiveworkshop.wc3.mpq.MpqCodebase; +import com.hiveworkshop.wc3.units.objectdata.War3ID; +import com.matrixeater.src.MainPanel; public class GLTFExport implements ActionListener { private final Logger log = Logger.getLogger(GLTFExport.class.getName()); @@ -39,6 +48,7 @@ public void actionPerformed(ActionEvent e) { log.info(this.getAllDoodadsPaths().size() + " doodad paths found for GLTF export."); var model0 = this.loadModel(this.getAllUnitPaths().get(0)); log.info("name: " + model0.getName()); + GltfModel gltfModel = createGltfModel(model0); } private List getAllUnitPaths() { @@ -60,7 +70,6 @@ private List getAllDoodadsPaths() { List doodadPaths = new ArrayList(); var data = mainframe.getDoodadData(); log.info("Doodad data: " + data.keySet().size()); - final War3ID modelFileId = War3ID.fromString("umdl"); for (var id : data.keySet()) { var obj = data.get(id); final int numberOfVariations = obj.getFieldAsInteger(War3ID.fromString("dvar"), 0); @@ -80,10 +89,88 @@ private List getAllDoodadsPaths() { return doodadPaths; } - private void processModel(EditableModel model) { - // Implement the logic to process the model and export it to GLTF format - log.info("Processing model for GLTF export: " + model.getName()); - // Add your GLTF export logic here + private static void export(EditableModel model, File selectedFile) throws IOException { + GltfModel gltfModel = createGltfModel(model); + try (OutputStream os = new FileOutputStream(selectedFile)) { + GltfModelWriter writer = new GltfModelWriter(); + writer.writeBinary(gltfModel, os); + } + } + + private static GltfModel createGltfModel(EditableModel model) { + // Create vertex data arrays + List geosets = new ArrayList(); + for (Geoset geoset : model.getGeosets()) { + geosets.add(geoset); + } + + // Count total vertices and triangles + int totalVertices = 0; + int totalTriangles = 0; + for (Geoset geoset : geosets) { + totalVertices += geoset.getVertices().size(); + totalTriangles += geoset.getTriangles().size(); + } + + // Create buffers + float[] positions = new float[totalVertices * 3]; + float[] normals = new float[totalVertices * 3]; + float[] uvs = new float[totalVertices * 2]; + int[] indices = new int[totalTriangles * 3]; + + int vertexIndex = 0; + int triangleIndex = 0; + int baseVertexOffset = 0; + + for (Geoset geoset : geosets) { + if (geoset.getVertices().size() == 0) { + continue; + } + + // Fill vertex data + for (GeosetVertex vertex : geoset.getVertices()) { + positions[vertexIndex * 3] = (float) vertex.x; + positions[vertexIndex * 3 + 1] = (float) vertex.y; + positions[vertexIndex * 3 + 2] = (float) vertex.z; + + if (vertex.getNormal() != null) { + normals[vertexIndex * 3] = (float) vertex.getNormal().x; + normals[vertexIndex * 3 + 1] = (float) vertex.getNormal().y; + normals[vertexIndex * 3 + 2] = (float) vertex.getNormal().z; + } else { + normals[vertexIndex * 3] = 0; + normals[vertexIndex * 3 + 1] = 0; + normals[vertexIndex * 3 + 2] = 1; + } + + if (vertex.getTverts().size() > 0) { + uvs[vertexIndex * 2] = (float) vertex.getTverts().get(0).x; + uvs[vertexIndex * 2 + 1] = (float) vertex.getTverts().get(0).y; + } else { + uvs[vertexIndex * 2] = 0; + uvs[vertexIndex * 2 + 1] = 0; + } + + vertexIndex++; + } + + // Fill triangle indices + for (Triangle triangle : geoset.getTriangles()) { + indices[triangleIndex * 3] = geoset.getVertices().indexOf(triangle.getVerts()[0]) + baseVertexOffset; + indices[triangleIndex * 3 + 1] = geoset.getVertices().indexOf(triangle.getVerts()[1]) + baseVertexOffset; + indices[triangleIndex * 3 + 2] = geoset.getVertices().indexOf(triangle.getVerts()[2]) + baseVertexOffset; + triangleIndex++; + } + + baseVertexOffset += geoset.getVertices().size(); + } + + // For now, return null until we can determine the correct jgltf API + // This is a placeholder that collects all the mesh data correctly + System.out.println("Created glTF data with " + (positions.length / 3) + " vertices and " + (indices.length / 3) + " triangles"); + + // TODO: Implement proper glTF model creation once the correct API is determined + return null; } private EditableModel loadModel(String path) { From e3ca18dda3875e18fac1178f253cb6ca0a611a3f Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Thu, 7 Aug 2025 05:56:19 +0300 Subject: [PATCH 05/33] Write a GLTF to file --- .../src/com/matrixeater/gltf/GLTFExport.java | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index 51d940fe..95c1d388 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -10,16 +10,16 @@ import java.util.List; import java.util.logging.Logger; -import javax.swing.JFileChooser; - import com.hiveworkshop.wc3.mdl.EditableModel; import com.hiveworkshop.wc3.mdl.Geoset; import com.hiveworkshop.wc3.mdl.GeosetVertex; import com.hiveworkshop.wc3.mdl.Triangle; import com.hiveworkshop.wc3.mdx.MdxUtils; +import de.javagl.jgltf.impl.v2.GlTF; import de.javagl.jgltf.model.GltfModel; import de.javagl.jgltf.model.io.GltfModelWriter; +import de.javagl.jgltf.model.io.GltfWriter; import de.wc3data.stream.BlizzardDataInputStream; import com.hiveworkshop.wc3.mpq.MpqCodebase; @@ -48,7 +48,11 @@ public void actionPerformed(ActionEvent e) { log.info(this.getAllDoodadsPaths().size() + " doodad paths found for GLTF export."); var model0 = this.loadModel(this.getAllUnitPaths().get(0)); log.info("name: " + model0.getName()); - GltfModel gltfModel = createGltfModel(model0); + try { + GLTFExport.export(model0); + } catch (IOException ex) { + log.severe("Failed to export model to GLTF: " + ex.getMessage()); + } } private List getAllUnitPaths() { @@ -89,15 +93,18 @@ private List getAllDoodadsPaths() { return doodadPaths; } - private static void export(EditableModel model, File selectedFile) throws IOException { - GltfModel gltfModel = createGltfModel(model); - try (OutputStream os = new FileOutputStream(selectedFile)) { - GltfModelWriter writer = new GltfModelWriter(); - writer.writeBinary(gltfModel, os); + private static void export(EditableModel model) throws IOException { + var gltf = createGltfModel(model); + File outputFile = new File("output.gltf"); + try (OutputStream os = new FileOutputStream(outputFile)) { + GltfWriter writer = new GltfWriter(); + writer.write(gltf, os); + } catch (IOException e) { + e.printStackTrace(); } } - private static GltfModel createGltfModel(EditableModel model) { + private static GlTF createGltfModel(EditableModel model) { // Create vertex data arrays List geosets = new ArrayList(); for (Geoset geoset : model.getGeosets()) { @@ -167,10 +174,14 @@ private static GltfModel createGltfModel(EditableModel model) { // For now, return null until we can determine the correct jgltf API // This is a placeholder that collects all the mesh data correctly - System.out.println("Created glTF data with " + (positions.length / 3) + " vertices and " + (indices.length / 3) + " triangles"); + GlTF gltf = new GlTF(); + gltf.setAsset(new de.javagl.jgltf.impl.v2.Asset()); - // TODO: Implement proper glTF model creation once the correct API is determined - return null; + + + System.out.println("Created glTF data with " + (positions.length / 3) + " vertices and " + (indices.length / 3) + " triangles"); + return gltf; + } private EditableModel loadModel(String path) { From da488a4d1a229d781eedf4b62f26974da5de9c1c Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Thu, 7 Aug 2025 07:04:18 +0300 Subject: [PATCH 06/33] Add mesh eporting, kinda --- .../src/com/matrixeater/gltf/GLTFExport.java | 94 +++++++++++++++++-- 1 file changed, 88 insertions(+), 6 deletions(-) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index 95c1d388..ba68918e 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -6,8 +6,12 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.logging.Logger; import com.hiveworkshop.wc3.mdl.EditableModel; @@ -16,7 +20,15 @@ import com.hiveworkshop.wc3.mdl.Triangle; import com.hiveworkshop.wc3.mdx.MdxUtils; +import de.javagl.jgltf.impl.v2.Accessor; +import de.javagl.jgltf.impl.v2.Asset; +import de.javagl.jgltf.impl.v2.Buffer; +import de.javagl.jgltf.impl.v2.BufferView; import de.javagl.jgltf.impl.v2.GlTF; +import de.javagl.jgltf.impl.v2.Mesh; +import de.javagl.jgltf.impl.v2.MeshPrimitive; +import de.javagl.jgltf.impl.v2.Node; +import de.javagl.jgltf.impl.v2.Scene; import de.javagl.jgltf.model.GltfModel; import de.javagl.jgltf.model.io.GltfModelWriter; import de.javagl.jgltf.model.io.GltfWriter; @@ -27,7 +39,7 @@ import com.matrixeater.src.MainPanel; public class GLTFExport implements ActionListener { - private final Logger log = Logger.getLogger(GLTFExport.class.getName()); + private final static Logger log = Logger.getLogger(GLTFExport.class.getName()); // I know it's bad, don't worry. private final MainPanel mainframe; @@ -95,7 +107,7 @@ private List getAllDoodadsPaths() { private static void export(EditableModel model) throws IOException { var gltf = createGltfModel(model); - File outputFile = new File("output.gltf"); + File outputFile = new File("ExportedFromRetera.gltf"); try (OutputStream os = new FileOutputStream(outputFile)) { GltfWriter writer = new GltfWriter(); writer.write(gltf, os); @@ -171,12 +183,81 @@ private static GlTF createGltfModel(EditableModel model) { baseVertexOffset += geoset.getVertices().size(); } - - // For now, return null until we can determine the correct jgltf API - // This is a placeholder that collects all the mesh data correctly + log.info(Arrays.stream(indices).max().orElse(-1) + " is the max index in indices array"); + log.info(Arrays.stream(indices).min().orElse(-1) + " is the min index in indices array"); + GlTF gltf = new GlTF(); - gltf.setAsset(new de.javagl.jgltf.impl.v2.Asset()); + Asset asset = new Asset(); + asset.setVersion("2.0"); + asset.setGenerator("MatrixEater GLTF Exporter"); + gltf.setAsset(asset); + + byte[] positionBytes = new byte[positions.length * 4]; + ByteBuffer.wrap(positionBytes).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer().put(positions); + String base64positionData = java.util.Base64.getEncoder().encodeToString(positionBytes); + String uri = "data:application/octet-stream;base64," + base64positionData; + + Buffer positionBuffer = new Buffer(); + positionBuffer.setByteLength(positionBytes.length); + positionBuffer.setUri(uri); + gltf.setBuffers(new ArrayList<>(Arrays.asList(positionBuffer))); + + BufferView positionBufferView = new BufferView(); + positionBufferView.setTarget(34962); // ARRAY_BUFFER + positionBufferView.setBuffer(0); + positionBufferView.setByteOffset(0); + positionBufferView.setByteLength(positionBytes.length); + gltf.setBufferViews(new ArrayList<>(Arrays.asList(positionBufferView))); + + Accessor positionAccessor = new Accessor(); + positionAccessor.setBufferView(0); + positionAccessor.setComponentType(5126); // FLOAT + positionAccessor.setCount(positions.length / 3); + positionAccessor.setType("VEC3"); + positionAccessor.setByteOffset(0); + gltf.setAccessors(new ArrayList<>(Arrays.asList(positionAccessor))); + byte[] indicesBytes = new byte[indices.length * 4]; + ByteBuffer.wrap(indicesBytes).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().put(indices); + String base64IndicesData = java.util.Base64.getEncoder().encodeToString(indicesBytes); + String indicesUri = "data:application/octet-stream;base64," + base64IndicesData; + + Buffer indicesBuffer = new Buffer(); + indicesBuffer.setByteLength(indicesBytes.length); + indicesBuffer.setUri(indicesUri); + gltf.getBuffers().add(indicesBuffer); // Updated to use getBuffers(), now it's not null because we added positionBuffer + + BufferView indicesBufferView = new BufferView(); + indicesBufferView.setTarget(34963); // ELEMENT_ARRAY_BUFFER + indicesBufferView.setBuffer(1); + indicesBufferView.setByteOffset(0); + indicesBufferView.setByteLength(indicesBytes.length); + gltf.getBufferViews().add(indicesBufferView); + + Accessor indicesAccessor = new Accessor(); + indicesAccessor.setBufferView(1); + indicesAccessor.setComponentType(5125); // UNSIGNED_SHORT + indicesAccessor.setCount(indices.length); + indicesAccessor.setType("SCALAR"); + indicesAccessor.setByteOffset(0); + gltf.getAccessors().add(indicesAccessor); + + Mesh mesh = new Mesh(); + MeshPrimitive primitive = new MeshPrimitive(); + primitive.setAttributes(Map.of("POSITION", 0)); + primitive.setIndices(1); + primitive.setMode(4); // TRIANGLES + mesh.setPrimitives(Arrays.asList(primitive)); + gltf.setMeshes(Arrays.asList(mesh)); + + Node node = new Node(); + node.setMesh(0); + gltf.setNodes(Arrays.asList(node)); + + Scene scene = new Scene(); + scene.setNodes(Arrays.asList(0)); + gltf.setScenes(Arrays.asList(scene)); + gltf.setScene(0); System.out.println("Created glTF data with " + (positions.length / 3) + " vertices and " + (indices.length / 3) + " triangles"); @@ -204,4 +285,5 @@ private String convertPathToMDX(String filepath) { } return filepath; } + } From 8a25b85ce2238a6acae14383cc1e855f348f485a Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Thu, 7 Aug 2025 18:18:37 +0300 Subject: [PATCH 07/33] Prepare for turning each geoset into a different mesh --- .../src/com/matrixeater/gltf/GLTFExport.java | 141 +++++++++++++----- 1 file changed, 100 insertions(+), 41 deletions(-) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index ba68918e..4026b658 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -29,8 +29,6 @@ import de.javagl.jgltf.impl.v2.MeshPrimitive; import de.javagl.jgltf.impl.v2.Node; import de.javagl.jgltf.impl.v2.Scene; -import de.javagl.jgltf.model.GltfModel; -import de.javagl.jgltf.model.io.GltfModelWriter; import de.javagl.jgltf.model.io.GltfWriter; import de.wc3data.stream.BlizzardDataInputStream; @@ -117,12 +115,25 @@ private static void export(EditableModel model) throws IOException { } private static GlTF createGltfModel(EditableModel model) { - // Create vertex data arrays + + GlTF gltf = new GlTF(); + Asset asset = new Asset(); + asset.setVersion("2.0"); + asset.setGenerator("MatrixEater GLTF Exporter"); + gltf.setAsset(asset); + + loadMeshIntoModel(model, gltf); + + return gltf; + } + + private static void loadMeshIntoModel(EditableModel model, GlTF gltf) { + // Create vertex data arrays List geosets = new ArrayList(); for (Geoset geoset : model.getGeosets()) { geosets.add(geoset); } - + // Count total vertices and triangles int totalVertices = 0; int totalTriangles = 0; @@ -130,28 +141,35 @@ private static GlTF createGltfModel(EditableModel model) { totalVertices += geoset.getVertices().size(); totalTriangles += geoset.getTriangles().size(); } - + // Create buffers float[] positions = new float[totalVertices * 3]; float[] normals = new float[totalVertices * 3]; float[] uvs = new float[totalVertices * 2]; int[] indices = new int[totalTriangles * 3]; - + int vertexIndex = 0; int triangleIndex = 0; int baseVertexOffset = 0; - + + log.info(Arrays.stream(indices).max().orElse(-1) + " is the max index in indices array"); + log.info(Arrays.stream(indices).min().orElse(-1) + " is the min index in indices array"); + List buffers = new ArrayList<>(); + List bufferViews = new ArrayList<>(); + List accessors = new ArrayList<>(); + List meshes = new ArrayList<>(); + List nodes = new ArrayList<>(); + List rootNodes = new ArrayList<>(); + for (Geoset geoset : geosets) { if (geoset.getVertices().size() == 0) { continue; } - // Fill vertex data for (GeosetVertex vertex : geoset.getVertices()) { positions[vertexIndex * 3] = (float) vertex.x; positions[vertexIndex * 3 + 1] = (float) vertex.y; positions[vertexIndex * 3 + 2] = (float) vertex.z; - if (vertex.getNormal() != null) { normals[vertexIndex * 3] = (float) vertex.getNormal().x; normals[vertexIndex * 3 + 1] = (float) vertex.getNormal().y; @@ -161,7 +179,6 @@ private static GlTF createGltfModel(EditableModel model) { normals[vertexIndex * 3 + 1] = 0; normals[vertexIndex * 3 + 2] = 1; } - if (vertex.getTverts().size() > 0) { uvs[vertexIndex * 2] = (float) vertex.getTverts().get(0).x; uvs[vertexIndex * 2 + 1] = (float) vertex.getTverts().get(0).y; @@ -169,28 +186,20 @@ private static GlTF createGltfModel(EditableModel model) { uvs[vertexIndex * 2] = 0; uvs[vertexIndex * 2 + 1] = 0; } - vertexIndex++; } - // Fill triangle indices for (Triangle triangle : geoset.getTriangles()) { indices[triangleIndex * 3] = geoset.getVertices().indexOf(triangle.getVerts()[0]) + baseVertexOffset; - indices[triangleIndex * 3 + 1] = geoset.getVertices().indexOf(triangle.getVerts()[1]) + baseVertexOffset; - indices[triangleIndex * 3 + 2] = geoset.getVertices().indexOf(triangle.getVerts()[2]) + baseVertexOffset; + indices[triangleIndex * 3 + 1] = geoset.getVertices().indexOf(triangle.getVerts()[1]) + + baseVertexOffset; + indices[triangleIndex * 3 + 2] = geoset.getVertices().indexOf(triangle.getVerts()[2]) + + baseVertexOffset; triangleIndex++; } - baseVertexOffset += geoset.getVertices().size(); } - log.info(Arrays.stream(indices).max().orElse(-1) + " is the max index in indices array"); - log.info(Arrays.stream(indices).min().orElse(-1) + " is the min index in indices array"); - GlTF gltf = new GlTF(); - Asset asset = new Asset(); - asset.setVersion("2.0"); - asset.setGenerator("MatrixEater GLTF Exporter"); - gltf.setAsset(asset); byte[] positionBytes = new byte[positions.length * 4]; ByteBuffer.wrap(positionBytes).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer().put(positions); @@ -200,23 +209,26 @@ private static GlTF createGltfModel(EditableModel model) { Buffer positionBuffer = new Buffer(); positionBuffer.setByteLength(positionBytes.length); positionBuffer.setUri(uri); - gltf.setBuffers(new ArrayList<>(Arrays.asList(positionBuffer))); + buffers.add(positionBuffer); + var vertexBufferIndex = buffers.size() - 1; BufferView positionBufferView = new BufferView(); positionBufferView.setTarget(34962); // ARRAY_BUFFER - positionBufferView.setBuffer(0); + positionBufferView.setBuffer(vertexBufferIndex); positionBufferView.setByteOffset(0); positionBufferView.setByteLength(positionBytes.length); - gltf.setBufferViews(new ArrayList<>(Arrays.asList(positionBufferView))); + bufferViews.add(positionBufferView); Accessor positionAccessor = new Accessor(); + // TODO: should define min/max values for positionAccessor, after I figure out + // how vertices are expresssed in the model positionAccessor.setBufferView(0); positionAccessor.setComponentType(5126); // FLOAT positionAccessor.setCount(positions.length / 3); positionAccessor.setType("VEC3"); positionAccessor.setByteOffset(0); - gltf.setAccessors(new ArrayList<>(Arrays.asList(positionAccessor))); - + accessors.add(positionAccessor); + byte[] indicesBytes = new byte[indices.length * 4]; ByteBuffer.wrap(indicesBytes).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().put(indices); String base64IndicesData = java.util.Base64.getEncoder().encodeToString(indicesBytes); @@ -225,14 +237,17 @@ private static GlTF createGltfModel(EditableModel model) { Buffer indicesBuffer = new Buffer(); indicesBuffer.setByteLength(indicesBytes.length); indicesBuffer.setUri(indicesUri); - gltf.getBuffers().add(indicesBuffer); // Updated to use getBuffers(), now it's not null because we added positionBuffer + buffers.add(indicesBuffer); + // gltf.getBuffers().add(indicesBuffer); // Updated to use getBuffers(), now it's not null because we added + // positionBuffer + var indicesBufferIndex = buffers.size() - 1; // Get the index of the indices buffer BufferView indicesBufferView = new BufferView(); indicesBufferView.setTarget(34963); // ELEMENT_ARRAY_BUFFER - indicesBufferView.setBuffer(1); + indicesBufferView.setBuffer(indicesBufferIndex); indicesBufferView.setByteOffset(0); indicesBufferView.setByteLength(indicesBytes.length); - gltf.getBufferViews().add(indicesBufferView); + bufferViews.add(indicesBufferView); Accessor indicesAccessor = new Accessor(); indicesAccessor.setBufferView(1); @@ -240,7 +255,7 @@ private static GlTF createGltfModel(EditableModel model) { indicesAccessor.setCount(indices.length); indicesAccessor.setType("SCALAR"); indicesAccessor.setByteOffset(0); - gltf.getAccessors().add(indicesAccessor); + accessors.add(indicesAccessor); Mesh mesh = new Mesh(); MeshPrimitive primitive = new MeshPrimitive(); @@ -248,21 +263,67 @@ private static GlTF createGltfModel(EditableModel model) { primitive.setIndices(1); primitive.setMode(4); // TRIANGLES mesh.setPrimitives(Arrays.asList(primitive)); - gltf.setMeshes(Arrays.asList(mesh)); + meshes.add(mesh); Node node = new Node(); node.setMesh(0); - gltf.setNodes(Arrays.asList(node)); - + nodes.add(node); + rootNodes.add(nodes.size() - 1); // Add the node to the root nodes list + Scene scene = new Scene(); - scene.setNodes(Arrays.asList(0)); + scene.setNodes(rootNodes); gltf.setScenes(Arrays.asList(scene)); gltf.setScene(0); + gltf.setBuffers(buffers); + gltf.setBufferViews(bufferViews); + gltf.setAccessors(accessors); + gltf.setMeshes(meshes); + gltf.setNodes(nodes); + } + private class GeosetData { + float[] positions; + float[] normals; + float[] uvs; + int[] indices; - System.out.println("Created glTF data with " + (positions.length / 3) + " vertices and " + (indices.length / 3) + " triangles"); - return gltf; - + public GeosetData(Geoset geoset) { + int vertexIndex = 0; + int triangleIndex = 0; + if (geoset.getVertices().size() == 0) { + return; + } + // Fill vertex data + for (GeosetVertex vertex : geoset.getVertices()) { + positions[vertexIndex * 3] = (float) vertex.x; + positions[vertexIndex * 3 + 1] = (float) vertex.y; + positions[vertexIndex * 3 + 2] = (float) vertex.z; + if (vertex.getNormal() != null) { + normals[vertexIndex * 3] = (float) vertex.getNormal().x; + normals[vertexIndex * 3 + 1] = (float) vertex.getNormal().y; + normals[vertexIndex * 3 + 2] = (float) vertex.getNormal().z; + } else { + normals[vertexIndex * 3] = 0; + normals[vertexIndex * 3 + 1] = 0; + normals[vertexIndex * 3 + 2] = 1; + } + if (vertex.getTverts().size() > 0) { + uvs[vertexIndex * 2] = (float) vertex.getTverts().get(0).x; + uvs[vertexIndex * 2 + 1] = (float) vertex.getTverts().get(0).y; + } else { + uvs[vertexIndex * 2] = 0; + uvs[vertexIndex * 2 + 1] = 0; + } + vertexIndex++; + } + // Fill triangle indices + for (Triangle triangle : geoset.getTriangles()) { + indices[triangleIndex * 3] = geoset.getVertices().indexOf(triangle.getVerts()[0]); + indices[triangleIndex * 3 + 1] = geoset.getVertices().indexOf(triangle.getVerts()[1]); + indices[triangleIndex * 3 + 2] = geoset.getVertices().indexOf(triangle.getVerts()[2]); + triangleIndex++; + } + } } private EditableModel loadModel(String path) { @@ -270,8 +331,7 @@ private EditableModel loadModel(String path) { try (BlizzardDataInputStream in = new BlizzardDataInputStream(f)) { final EditableModel model = new EditableModel(MdxUtils.loadModel(in)); return model; - } - catch (Exception e) { + } catch (Exception e) { log.severe("Failed to load model from path: " + path + " due to " + e.getMessage()); return null; } @@ -285,5 +345,4 @@ private String convertPathToMDX(String filepath) { } return filepath; } - } From a4d6c36640e7b61ccbc39073368d2216a51dacb3 Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Thu, 7 Aug 2025 18:26:47 +0300 Subject: [PATCH 08/33] change naming --- matrixeater/src/com/matrixeater/gltf/GLTFExport.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index 4026b658..3602c242 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -105,7 +105,7 @@ private List getAllDoodadsPaths() { private static void export(EditableModel model) throws IOException { var gltf = createGltfModel(model); - File outputFile = new File("ExportedFromRetera.gltf"); + File outputFile = new File(model.getName() + ".gltf"); try (OutputStream os = new FileOutputStream(outputFile)) { GltfWriter writer = new GltfWriter(); writer.write(gltf, os); @@ -119,7 +119,7 @@ private static GlTF createGltfModel(EditableModel model) { GlTF gltf = new GlTF(); Asset asset = new Asset(); asset.setVersion("2.0"); - asset.setGenerator("MatrixEater GLTF Exporter"); + asset.setGenerator(model.getName()); gltf.setAsset(asset); loadMeshIntoModel(model, gltf); From c4e24db3b4fa75cd697c29fb6ac7a0eba0244f6f Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Thu, 7 Aug 2025 18:44:01 +0300 Subject: [PATCH 09/33] Add a root node to gltf --- .../src/com/matrixeater/gltf/GLTFExport.java | 204 ++++++++---------- 1 file changed, 88 insertions(+), 116 deletions(-) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index 3602c242..09fb2cf3 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -142,136 +142,103 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf) { totalTriangles += geoset.getTriangles().size(); } - // Create buffers - float[] positions = new float[totalVertices * 3]; - float[] normals = new float[totalVertices * 3]; - float[] uvs = new float[totalVertices * 2]; - int[] indices = new int[totalTriangles * 3]; - int vertexIndex = 0; int triangleIndex = 0; int baseVertexOffset = 0; - log.info(Arrays.stream(indices).max().orElse(-1) + " is the max index in indices array"); - log.info(Arrays.stream(indices).min().orElse(-1) + " is the min index in indices array"); List buffers = new ArrayList<>(); List bufferViews = new ArrayList<>(); List accessors = new ArrayList<>(); List meshes = new ArrayList<>(); List nodes = new ArrayList<>(); - List rootNodes = new ArrayList<>(); + List geoNodes = new ArrayList<>(); // called geo because it contains nodes made form geosets for (Geoset geoset : geosets) { - if (geoset.getVertices().size() == 0) { - continue; - } - // Fill vertex data - for (GeosetVertex vertex : geoset.getVertices()) { - positions[vertexIndex * 3] = (float) vertex.x; - positions[vertexIndex * 3 + 1] = (float) vertex.y; - positions[vertexIndex * 3 + 2] = (float) vertex.z; - if (vertex.getNormal() != null) { - normals[vertexIndex * 3] = (float) vertex.getNormal().x; - normals[vertexIndex * 3 + 1] = (float) vertex.getNormal().y; - normals[vertexIndex * 3 + 2] = (float) vertex.getNormal().z; - } else { - normals[vertexIndex * 3] = 0; - normals[vertexIndex * 3 + 1] = 0; - normals[vertexIndex * 3 + 2] = 1; - } - if (vertex.getTverts().size() > 0) { - uvs[vertexIndex * 2] = (float) vertex.getTverts().get(0).x; - uvs[vertexIndex * 2 + 1] = (float) vertex.getTverts().get(0).y; - } else { - uvs[vertexIndex * 2] = 0; - uvs[vertexIndex * 2 + 1] = 0; - } - vertexIndex++; - } - // Fill triangle indices - for (Triangle triangle : geoset.getTriangles()) { - indices[triangleIndex * 3] = geoset.getVertices().indexOf(triangle.getVerts()[0]) + baseVertexOffset; - indices[triangleIndex * 3 + 1] = geoset.getVertices().indexOf(triangle.getVerts()[1]) - + baseVertexOffset; - indices[triangleIndex * 3 + 2] = geoset.getVertices().indexOf(triangle.getVerts()[2]) - + baseVertexOffset; - triangleIndex++; - } - baseVertexOffset += geoset.getVertices().size(); + var data = new GeosetData(geoset); + byte[] positionBytes = new byte[data.positions.length * 4]; + ByteBuffer.wrap(positionBytes).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer().put(data.positions); + String base64positionData = java.util.Base64.getEncoder().encodeToString(positionBytes); + String uri = "data:application/octet-stream;base64," + base64positionData; + + Buffer positionBuffer = new Buffer(); + positionBuffer.setByteLength(positionBytes.length); + positionBuffer.setUri(uri); + buffers.add(positionBuffer); + var vertexBufferIndex = buffers.size() - 1; + + BufferView positionBufferView = new BufferView(); + positionBufferView.setTarget(34962); // ARRAY_BUFFER + positionBufferView.setBuffer(vertexBufferIndex); + positionBufferView.setByteOffset(0); + positionBufferView.setByteLength(positionBytes.length); + bufferViews.add(positionBufferView); + var positionBufferViewIndex = bufferViews.size() - 1; + + Accessor positionAccessor = new Accessor(); + // TODO: should define min/max values for positionAccessor, after I figure out + // how vertices are expresssed in the model + positionAccessor.setBufferView(positionBufferViewIndex); + positionAccessor.setComponentType(5126); // FLOAT + positionAccessor.setCount(data.positions.length / 3); + positionAccessor.setType("VEC3"); + positionAccessor.setByteOffset(0); + accessors.add(positionAccessor); + var positionAccessorIndex = accessors.size() - 1; + + byte[] indicesBytes = new byte[data.indices.length * 4]; + ByteBuffer.wrap(indicesBytes).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().put(data.indices); + String base64IndicesData = java.util.Base64.getEncoder().encodeToString(indicesBytes); + String indicesUri = "data:application/octet-stream;base64," + base64IndicesData; + + Buffer indicesBuffer = new Buffer(); + indicesBuffer.setByteLength(indicesBytes.length); + indicesBuffer.setUri(indicesUri); + buffers.add(indicesBuffer); + // gltf.getBuffers().add(indicesBuffer); // Updated to use getBuffers(), now + // it's not null because we added + // positionBuffer + var indicesBufferIndex = buffers.size() - 1; // Get the index of the indices buffer + + BufferView indicesBufferView = new BufferView(); + indicesBufferView.setTarget(34963); // ELEMENT_ARRAY_BUFFER + indicesBufferView.setBuffer(indicesBufferIndex); + indicesBufferView.setByteOffset(0); + indicesBufferView.setByteLength(indicesBytes.length); + bufferViews.add(indicesBufferView); + var indicesBufferViewIndex = bufferViews.size() - 1; // Get the index of the indices buffer view + + Accessor indicesAccessor = new Accessor(); + indicesAccessor.setBufferView(indicesBufferViewIndex); + indicesAccessor.setComponentType(5125); // UNSIGNED_SHORT + indicesAccessor.setCount(data.indices.length); + indicesAccessor.setType("SCALAR"); + indicesAccessor.setByteOffset(0); + accessors.add(indicesAccessor); + var indicesAccessorIndex = accessors.size() - 1; // Get the index of the indices accessor + + Mesh mesh = new Mesh(); + MeshPrimitive primitive = new MeshPrimitive(); + primitive.setAttributes(Map.of("POSITION", positionAccessorIndex)); + primitive.setIndices(indicesAccessorIndex); + primitive.setMode(4); // TRIANGLES + mesh.setPrimitives(Arrays.asList(primitive)); + meshes.add(mesh); + var meshIndex = meshes.size() - 1; // Get the index of the mesh + + Node node = new Node(); + node.setMesh(meshIndex); + nodes.add(node); + geoNodes.add(nodes.size() - 1); // Add the node to the root nodes list } - - byte[] positionBytes = new byte[positions.length * 4]; - ByteBuffer.wrap(positionBytes).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer().put(positions); - String base64positionData = java.util.Base64.getEncoder().encodeToString(positionBytes); - String uri = "data:application/octet-stream;base64," + base64positionData; - - Buffer positionBuffer = new Buffer(); - positionBuffer.setByteLength(positionBytes.length); - positionBuffer.setUri(uri); - buffers.add(positionBuffer); - - var vertexBufferIndex = buffers.size() - 1; - BufferView positionBufferView = new BufferView(); - positionBufferView.setTarget(34962); // ARRAY_BUFFER - positionBufferView.setBuffer(vertexBufferIndex); - positionBufferView.setByteOffset(0); - positionBufferView.setByteLength(positionBytes.length); - bufferViews.add(positionBufferView); - - Accessor positionAccessor = new Accessor(); - // TODO: should define min/max values for positionAccessor, after I figure out - // how vertices are expresssed in the model - positionAccessor.setBufferView(0); - positionAccessor.setComponentType(5126); // FLOAT - positionAccessor.setCount(positions.length / 3); - positionAccessor.setType("VEC3"); - positionAccessor.setByteOffset(0); - accessors.add(positionAccessor); - - byte[] indicesBytes = new byte[indices.length * 4]; - ByteBuffer.wrap(indicesBytes).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer().put(indices); - String base64IndicesData = java.util.Base64.getEncoder().encodeToString(indicesBytes); - String indicesUri = "data:application/octet-stream;base64," + base64IndicesData; - - Buffer indicesBuffer = new Buffer(); - indicesBuffer.setByteLength(indicesBytes.length); - indicesBuffer.setUri(indicesUri); - buffers.add(indicesBuffer); - // gltf.getBuffers().add(indicesBuffer); // Updated to use getBuffers(), now it's not null because we added - // positionBuffer - var indicesBufferIndex = buffers.size() - 1; // Get the index of the indices buffer - - BufferView indicesBufferView = new BufferView(); - indicesBufferView.setTarget(34963); // ELEMENT_ARRAY_BUFFER - indicesBufferView.setBuffer(indicesBufferIndex); - indicesBufferView.setByteOffset(0); - indicesBufferView.setByteLength(indicesBytes.length); - bufferViews.add(indicesBufferView); - - Accessor indicesAccessor = new Accessor(); - indicesAccessor.setBufferView(1); - indicesAccessor.setComponentType(5125); // UNSIGNED_SHORT - indicesAccessor.setCount(indices.length); - indicesAccessor.setType("SCALAR"); - indicesAccessor.setByteOffset(0); - accessors.add(indicesAccessor); - - Mesh mesh = new Mesh(); - MeshPrimitive primitive = new MeshPrimitive(); - primitive.setAttributes(Map.of("POSITION", 0)); - primitive.setIndices(1); - primitive.setMode(4); // TRIANGLES - mesh.setPrimitives(Arrays.asList(primitive)); - meshes.add(mesh); - - Node node = new Node(); - node.setMesh(0); - nodes.add(node); - rootNodes.add(nodes.size() - 1); // Add the node to the root nodes list + Node rootNode = new Node(); + rootNode.setName(model.getName()); + rootNode.setChildren(geoNodes); + nodes.add(rootNode); + var rootNodeIndex = nodes.size() - 1; // Get the index of the root node Scene scene = new Scene(); - scene.setNodes(rootNodes); + scene.setNodes(Arrays.asList(rootNodeIndex)); gltf.setScenes(Arrays.asList(scene)); gltf.setScene(0); @@ -281,13 +248,18 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf) { gltf.setMeshes(meshes); gltf.setNodes(nodes); } - private class GeosetData { + + private static class GeosetData { float[] positions; float[] normals; float[] uvs; int[] indices; public GeosetData(Geoset geoset) { + positions = new float[geoset.getVertices().size() * 3]; + normals = new float[geoset.getVertices().size() * 3]; + uvs = new float[geoset.getVertices().size() * 2]; + indices = new int[geoset.getTriangles().size() * 3]; int vertexIndex = 0; int triangleIndex = 0; if (geoset.getVertices().size() == 0) { From a30e289882e7551334ec94bce26f38e4da42ad2a Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Thu, 7 Aug 2025 21:04:45 +0300 Subject: [PATCH 10/33] log bones --- .../src/com/matrixeater/gltf/GLTFExport.java | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index 09fb2cf3..72580bf2 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -14,9 +14,11 @@ import java.util.Map; import java.util.logging.Logger; +import com.hiveworkshop.wc3.mdl.Bone; import com.hiveworkshop.wc3.mdl.EditableModel; import com.hiveworkshop.wc3.mdl.Geoset; import com.hiveworkshop.wc3.mdl.GeosetVertex; +import com.hiveworkshop.wc3.mdl.IdObject; import com.hiveworkshop.wc3.mdl.Triangle; import com.hiveworkshop.wc3.mdx.MdxUtils; @@ -56,7 +58,7 @@ public void actionPerformed(ActionEvent e) { // } log.info(this.getAllUnitPaths().size() + " unit paths found for GLTF export."); log.info(this.getAllDoodadsPaths().size() + " doodad paths found for GLTF export."); - var model0 = this.loadModel(this.getAllUnitPaths().get(0)); + var model0 = this.loadModel(this.getAllUnitPaths().get(689)); log.info("name: " + model0.getName()); try { GLTFExport.export(model0); @@ -128,23 +130,14 @@ private static GlTF createGltfModel(EditableModel model) { } private static void loadMeshIntoModel(EditableModel model, GlTF gltf) { - // Create vertex data arrays - List geosets = new ArrayList(); - for (Geoset geoset : model.getGeosets()) { - geosets.add(geoset); - } - // Count total vertices and triangles - int totalVertices = 0; - int totalTriangles = 0; - for (Geoset geoset : geosets) { - totalVertices += geoset.getVertices().size(); - totalTriangles += geoset.getTriangles().size(); + List bones = new ArrayList<>(); + for (final IdObject object : model.getIdObjects()) { + if (object instanceof Bone) { + bones.add((Bone) object); + } } - - int vertexIndex = 0; - int triangleIndex = 0; - int baseVertexOffset = 0; + log.info("Bones: " + bones.size()); List buffers = new ArrayList<>(); List bufferViews = new ArrayList<>(); @@ -152,8 +145,8 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf) { List meshes = new ArrayList<>(); List nodes = new ArrayList<>(); List geoNodes = new ArrayList<>(); // called geo because it contains nodes made form geosets - - for (Geoset geoset : geosets) { + log.info("Geosets: " + model.getGeosets().size()); + for (Geoset geoset : model.getGeosets()) { var data = new GeosetData(geoset); byte[] positionBytes = new byte[data.positions.length * 4]; ByteBuffer.wrap(positionBytes).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer().put(data.positions); From 9c23af47495600403468793072a5fbe0efea1367 Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Fri, 8 Aug 2025 10:40:06 +0300 Subject: [PATCH 11/33] Modify vertex primitive to inclue uv data --- .../src/com/matrixeater/gltf/GLTFExport.java | 78 ++++++++++++++++--- 1 file changed, 69 insertions(+), 9 deletions(-) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index 72580bf2..380ebf85 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -22,6 +22,7 @@ import com.hiveworkshop.wc3.mdl.Triangle; import com.hiveworkshop.wc3.mdx.MdxUtils; +import de.javagl.jgltf.impl.v2.Skin; import de.javagl.jgltf.impl.v2.Accessor; import de.javagl.jgltf.impl.v2.Asset; import de.javagl.jgltf.impl.v2.Buffer; @@ -131,20 +132,15 @@ private static GlTF createGltfModel(EditableModel model) { private static void loadMeshIntoModel(EditableModel model, GlTF gltf) { - List bones = new ArrayList<>(); - for (final IdObject object : model.getIdObjects()) { - if (object instanceof Bone) { - bones.add((Bone) object); - } - } - log.info("Bones: " + bones.size()); - List buffers = new ArrayList<>(); List bufferViews = new ArrayList<>(); List accessors = new ArrayList<>(); List meshes = new ArrayList<>(); List nodes = new ArrayList<>(); List geoNodes = new ArrayList<>(); // called geo because it contains nodes made form geosets + List skins = new ArrayList<>(); + + // MESH log.info("Geosets: " + model.getGeosets().size()); for (Geoset geoset : model.getGeosets()) { var data = new GeosetData(geoset); @@ -209,9 +205,37 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf) { accessors.add(indicesAccessor); var indicesAccessorIndex = accessors.size() - 1; // Get the index of the indices accessor + byte[] uvBytes = new byte[data.uvs.length * 4]; + ByteBuffer.wrap(uvBytes).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer().put(data.uvs); + String base64UvData = java.util.Base64.getEncoder().encodeToString(uvBytes); + String uvUri = "data:application/octet-stream;base64," + base64UvData; + + Buffer uvBuffer = new Buffer(); + uvBuffer.setByteLength(uvBytes.length); + uvBuffer.setUri(uvUri); + buffers.add(uvBuffer); + var uvBufferIndex = buffers.size() - 1; // Get the index of the + + BufferView uvBufferView = new BufferView(); + uvBufferView.setTarget(34962); // ARRAY_BUFFER + uvBufferView.setBuffer(uvBufferIndex); + uvBufferView.setByteOffset(0); + uvBufferView.setByteLength(uvBytes.length); + bufferViews.add(uvBufferView); + var uvBufferViewIndex = bufferViews.size() - 1; // Get the index + + Accessor uvAccessor = new Accessor(); + uvAccessor.setBufferView(uvBufferViewIndex); + uvAccessor.setComponentType(5126); + uvAccessor.setCount(data.uvs.length / 2); + uvAccessor.setType("VEC2"); + uvAccessor.setByteOffset(0); + accessors.add(uvAccessor); + var uvAccessorIndex = accessors.size() - 1; + Mesh mesh = new Mesh(); MeshPrimitive primitive = new MeshPrimitive(); - primitive.setAttributes(Map.of("POSITION", positionAccessorIndex)); + primitive.setAttributes(Map.of("POSITION", positionAccessorIndex, "TEXCOORD_0", uvAccessorIndex)); primitive.setIndices(indicesAccessorIndex); primitive.setMode(4); // TRIANGLES mesh.setPrimitives(Arrays.asList(primitive)); @@ -220,10 +244,45 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf) { Node node = new Node(); node.setMesh(meshIndex); + node.setName(geoset.getName()); nodes.add(node); geoNodes.add(nodes.size() - 1); // Add the node to the root nodes list } + // Bones + List mdxBones = new ArrayList<>(); + for (final IdObject object : model.getIdObjects()) { + if (object instanceof Bone) { + mdxBones.add((Bone) object); + } + } + + log.info("Bones: " + mdxBones.size()); + // if (mdxBones.size() > 0) { + // // Create a skin for the bones + // Skin skin = new Skin(); + // List jointIndices = new ArrayList<>(); + // List inverseBindMatrices = new ArrayList<>(); // Changed to List for inverse bind matrices + // for (Bone bone : mdxBones) { + // jointIndices.add(nodes.size()); // Add the node index to the joint indices + // Node boneNode = new Node(); + // boneNode.setName(bone.getName()); + // nodes.add(boneNode); + // // Create an inverse bind matrix for the bone + // float[] inverseBindMatrix = new float[16]; + // // Assuming the bone has a method to get its transformation matrix + // // Here we just create an identity matrix for simplicity + // for (int i = 0; i < 16; i++) { + // inverseBindMatrix[i] = (i % 5 == 0) ? 1 + // : 0; // Identity matrix + // } + // inverseBindMatrices.add(inverseBindMatrix); + // } + // skins.add(skin); + // } + + + // Merge Node rootNode = new Node(); rootNode.setName(model.getName()); rootNode.setChildren(geoNodes); @@ -240,6 +299,7 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf) { gltf.setAccessors(accessors); gltf.setMeshes(meshes); gltf.setNodes(nodes); + //gltf.setSkins(skins); } private static class GeosetData { From f168bc01aa0111c4cb6c02211991ecb33d4be394 Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Fri, 8 Aug 2025 11:28:38 +0300 Subject: [PATCH 12/33] begin implementation of material parser --- .../src/com/matrixeater/gltf/GLTFExport.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index 380ebf85..164c6b4c 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -28,11 +28,13 @@ import de.javagl.jgltf.impl.v2.Buffer; import de.javagl.jgltf.impl.v2.BufferView; import de.javagl.jgltf.impl.v2.GlTF; +import de.javagl.jgltf.impl.v2.Material; import de.javagl.jgltf.impl.v2.Mesh; import de.javagl.jgltf.impl.v2.MeshPrimitive; import de.javagl.jgltf.impl.v2.Node; import de.javagl.jgltf.impl.v2.Scene; import de.javagl.jgltf.model.io.GltfWriter; +import de.javagl.jgltf.impl.v2.*; import de.wc3data.stream.BlizzardDataInputStream; import com.hiveworkshop.wc3.mpq.MpqCodebase; @@ -138,8 +140,27 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf) { List meshes = new ArrayList<>(); List nodes = new ArrayList<>(); List geoNodes = new ArrayList<>(); // called geo because it contains nodes made form geosets + List materials = new ArrayList<>(); List skins = new ArrayList<>(); + //Materials + for (var material: model.getMaterials()) + { + if (material.getLayers().size() > 1) + { + log.warning("Material " + material.getName() + " has more than one layer, which is not supported in GLTF export."); + } + Material glMaterial = new Material(); + glMaterial.setName(material.getName()); + + MaterialPbrMetallicRoughness pbr = new MaterialPbrMetallicRoughness(); + pbr.setBaseColorFactor(new float[] {1.0f, 0.0f, 0.0f, 1.0f}); + pbr.setMetallicFactor(0.0f); + pbr.setRoughnessFactor(1.0f); + + glMaterial.setPbrMetallicRoughness(pbr); + materials.add(glMaterial); + } // MESH log.info("Geosets: " + model.getGeosets().size()); for (Geoset geoset : model.getGeosets()) { @@ -238,6 +259,7 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf) { primitive.setAttributes(Map.of("POSITION", positionAccessorIndex, "TEXCOORD_0", uvAccessorIndex)); primitive.setIndices(indicesAccessorIndex); primitive.setMode(4); // TRIANGLES + primitive.setMaterial(data.materialIndex); // Assuming materialIndex is the index of the material in the glTF mesh.setPrimitives(Arrays.asList(primitive)); meshes.add(mesh); var meshIndex = meshes.size() - 1; // Get the index of the mesh @@ -299,6 +321,7 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf) { gltf.setAccessors(accessors); gltf.setMeshes(meshes); gltf.setNodes(nodes); + gltf.setMaterials(materials); //gltf.setSkins(skins); } @@ -307,12 +330,14 @@ private static class GeosetData { float[] normals; float[] uvs; int[] indices; + int materialIndex; public GeosetData(Geoset geoset) { positions = new float[geoset.getVertices().size() * 3]; normals = new float[geoset.getVertices().size() * 3]; uvs = new float[geoset.getVertices().size() * 2]; indices = new int[geoset.getTriangles().size() * 3]; + materialIndex = geoset.getMaterialID(); int vertexIndex = 0; int triangleIndex = 0; if (geoset.getVertices().size() == 0) { From 37f4642e2163e422ee5d113f71c04064f75afc44 Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Fri, 8 Aug 2025 14:34:09 +0300 Subject: [PATCH 13/33] Add texture --- .../src/com/matrixeater/gltf/GLTFExport.java | 128 ++++++++++++++---- 1 file changed, 98 insertions(+), 30 deletions(-) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index 164c6b4c..eaecf5da 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -2,6 +2,7 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -14,6 +15,7 @@ import java.util.Map; import java.util.logging.Logger; +import com.hiveworkshop.wc3.gui.datachooser.DataSource; import com.hiveworkshop.wc3.mdl.Bone; import com.hiveworkshop.wc3.mdl.EditableModel; import com.hiveworkshop.wc3.mdl.Geoset; @@ -62,6 +64,7 @@ public void actionPerformed(ActionEvent e) { log.info(this.getAllUnitPaths().size() + " unit paths found for GLTF export."); log.info(this.getAllDoodadsPaths().size() + " doodad paths found for GLTF export."); var model0 = this.loadModel(this.getAllUnitPaths().get(689)); + log.info("name: " + model0.getName()); try { GLTFExport.export(model0); @@ -140,23 +143,73 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf) { List meshes = new ArrayList<>(); List nodes = new ArrayList<>(); List geoNodes = new ArrayList<>(); // called geo because it contains nodes made form geosets + List images = new ArrayList<>(); + List samplers = new ArrayList<>(); + List textures = new ArrayList<>(); List materials = new ArrayList<>(); List skins = new ArrayList<>(); - //Materials - for (var material: model.getMaterials()) - { - if (material.getLayers().size() > 1) - { - log.warning("Material " + material.getName() + " has more than one layer, which is not supported in GLTF export."); + // Materials + for (var material : model.getMaterials()) { + if (material.getLayers().size() > 1) { + log.warning("Material " + material.getName() + + " has more than one layer, which is not supported in GLTF export, conversion might have errors."); } + + var pngBytes = getPngFromMaterial(material, model.getWrappedDataSource()); + Buffer materialTextureBuffer = new Buffer(); + String pngBase64Data = java.util.Base64.getEncoder().encodeToString(pngBytes); + String uri = "data:application/octet-stream;base64," + pngBase64Data; + materialTextureBuffer.setByteLength(pngBytes.length); + materialTextureBuffer.setUri(uri); + buffers.add(materialTextureBuffer); + var materialTextureBufferIndex = buffers.size() - 1; + + BufferView materialTextureBufferView = new BufferView(); + materialTextureBufferView.setBuffer(materialTextureBufferIndex); + materialTextureBufferView.setByteOffset(0); + materialTextureBufferView.setByteLength(pngBytes.length); + bufferViews.add(materialTextureBufferView); + var materialTextureBufferViewIndex = bufferViews.size() - 1; + + Image materialImage = new Image(); + materialImage.setBufferView(materialTextureBufferViewIndex); + materialImage.setMimeType("image/png"); + images.add(materialImage); + var materialImageIndex = images.size() - 1; + + Sampler sampler = new Sampler(); + sampler.setMagFilter(9729); // LINEAR + sampler.setMinFilter(9987); // LINEAR_MIPMAP_LINEAR + sampler.setWrapS(10497); // REPEAT + sampler.setWrapT(10497); // REPEAT + samplers.add(sampler); + var samplerIndex = samplers.size() - 1; + + Texture texture = new Texture(); + texture.setSource(materialImageIndex); + texture.setSampler(samplerIndex); + textures.add(texture); + var textureIndex = textures.size() - 1; + + TextureInfo textureInfo = new TextureInfo(); + textureInfo.setIndex(textureIndex); + Material glMaterial = new Material(); glMaterial.setName(material.getName()); + // glMaterial.setAlphaMode("BLEND"); + // glMaterial.setAlphaCutoff(null); + //! The best approximation I could find so far + glMaterial.setAlphaMode("MASK"); + glMaterial.setAlphaCutoff(0.5f); // or whatever cutoff works best + MaterialPbrMetallicRoughness pbr = new MaterialPbrMetallicRoughness(); - pbr.setBaseColorFactor(new float[] {1.0f, 0.0f, 0.0f, 1.0f}); + pbr.setBaseColorFactor(new float[]{1, 1, 1, 1}); pbr.setMetallicFactor(0.0f); pbr.setRoughnessFactor(1.0f); + pbr.setBaseColorTexture(textureInfo); + glMaterial.setPbrMetallicRoughness(pbr); materials.add(glMaterial); @@ -259,7 +312,8 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf) { primitive.setAttributes(Map.of("POSITION", positionAccessorIndex, "TEXCOORD_0", uvAccessorIndex)); primitive.setIndices(indicesAccessorIndex); primitive.setMode(4); // TRIANGLES - primitive.setMaterial(data.materialIndex); // Assuming materialIndex is the index of the material in the glTF + primitive.setMaterial(data.materialIndex); // Assuming materialIndex is the index of the material in the + // glTF mesh.setPrimitives(Arrays.asList(primitive)); meshes.add(mesh); var meshIndex = meshes.size() - 1; // Get the index of the mesh @@ -281,28 +335,28 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf) { log.info("Bones: " + mdxBones.size()); // if (mdxBones.size() > 0) { - // // Create a skin for the bones - // Skin skin = new Skin(); - // List jointIndices = new ArrayList<>(); - // List inverseBindMatrices = new ArrayList<>(); // Changed to List for inverse bind matrices - // for (Bone bone : mdxBones) { - // jointIndices.add(nodes.size()); // Add the node index to the joint indices - // Node boneNode = new Node(); - // boneNode.setName(bone.getName()); - // nodes.add(boneNode); - // // Create an inverse bind matrix for the bone - // float[] inverseBindMatrix = new float[16]; - // // Assuming the bone has a method to get its transformation matrix - // // Here we just create an identity matrix for simplicity - // for (int i = 0; i < 16; i++) { - // inverseBindMatrix[i] = (i % 5 == 0) ? 1 - // : 0; // Identity matrix - // } - // inverseBindMatrices.add(inverseBindMatrix); - // } - // skins.add(skin); + // // Create a skin for the bones + // Skin skin = new Skin(); + // List jointIndices = new ArrayList<>(); + // List inverseBindMatrices = new ArrayList<>(); // Changed to + // List for inverse bind matrices + // for (Bone bone : mdxBones) { + // jointIndices.add(nodes.size()); // Add the node index to the joint indices + // Node boneNode = new Node(); + // boneNode.setName(bone.getName()); + // nodes.add(boneNode); + // // Create an inverse bind matrix for the bone + // float[] inverseBindMatrix = new float[16]; + // // Assuming the bone has a method to get its transformation matrix + // // Here we just create an identity matrix for simplicity + // for (int i = 0; i < 16; i++) { + // inverseBindMatrix[i] = (i % 5 == 0) ? 1 + // : 0; // Identity matrix + // } + // inverseBindMatrices.add(inverseBindMatrix); + // } + // skins.add(skin); // } - // Merge Node rootNode = new Node(); @@ -321,8 +375,11 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf) { gltf.setAccessors(accessors); gltf.setMeshes(meshes); gltf.setNodes(nodes); + gltf.setImages(images); + gltf.setSamplers(samplers); + gltf.setTextures(textures); gltf.setMaterials(materials); - //gltf.setSkins(skins); + // gltf.setSkins(skins); } private static class GeosetData { @@ -376,6 +433,17 @@ public GeosetData(Geoset geoset) { } } + private static byte[] getPngFromMaterial(com.hiveworkshop.wc3.mdl.Material material, DataSource dataSource) { + var img = material.getBufferedImage(dataSource); + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + javax.imageio.ImageIO.write(img, "png", baos); + return baos.toByteArray(); + } catch (IOException e) { + log.severe("Failed to write image for material " + material.getName() + ": " + e.getMessage()); + return null; + } + } + private EditableModel loadModel(String path) { var f = MpqCodebase.get().getResourceAsStream(path); try (BlizzardDataInputStream in = new BlizzardDataInputStream(f)) { From 6c5a7099215dd63dca3431a611e9382f03fe9b03 Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Fri, 8 Aug 2025 21:40:03 +0300 Subject: [PATCH 14/33] add rotation --- matrixeater/src/com/matrixeater/gltf/GLTFExport.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index eaecf5da..a81ca36b 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -362,6 +362,8 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf) { Node rootNode = new Node(); rootNode.setName(model.getName()); rootNode.setChildren(geoNodes); + // Rotate -90 degrees around X-axis to convert from Z-up (MDX) to Y-up (glTF) + rootNode.setRotation(new float[] { -0.7071068f, 0, 0, 0.7071068f }); nodes.add(rootNode); var rootNodeIndex = nodes.size() - 1; // Get the index of the root node From fb30f21744572f73511d3821d470b8c58e9d2dcc Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Fri, 8 Aug 2025 23:59:19 +0300 Subject: [PATCH 15/33] add check for is mesh available during animation to neatly export only standing anim --- .../src/com/matrixeater/gltf/GLTFExport.java | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index a81ca36b..993cc544 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -19,6 +19,7 @@ import com.hiveworkshop.wc3.mdl.Bone; import com.hiveworkshop.wc3.mdl.EditableModel; import com.hiveworkshop.wc3.mdl.Geoset; +import com.hiveworkshop.wc3.mdl.GeosetAnim; import com.hiveworkshop.wc3.mdl.GeosetVertex; import com.hiveworkshop.wc3.mdl.IdObject; import com.hiveworkshop.wc3.mdl.Triangle; @@ -35,6 +36,7 @@ import de.javagl.jgltf.impl.v2.MeshPrimitive; import de.javagl.jgltf.impl.v2.Node; import de.javagl.jgltf.impl.v2.Scene; +import de.javagl.jgltf.model.animation.AnimationManager.AnimationPolicy; import de.javagl.jgltf.model.io.GltfWriter; import de.javagl.jgltf.impl.v2.*; import de.wc3data.stream.BlizzardDataInputStream; @@ -64,6 +66,18 @@ public void actionPerformed(ActionEvent e) { log.info(this.getAllUnitPaths().size() + " unit paths found for GLTF export."); log.info(this.getAllDoodadsPaths().size() + " doodad paths found for GLTF export."); var model0 = this.loadModel(this.getAllUnitPaths().get(689)); + var anim = model0.getAnim(0); + var corpse = model0.getGeoset(0);// last geoset is a corpse, shouldn't be visible in a walk + log.info("Geoset" + corpse.getName() + " visibility: " + + isGeosetVisibleInAnimation(corpse, anim)); + int visibilityCount = 0; + for (Geoset geoset : model0.getGeosets()) { + if (isGeosetVisibleInAnimation(geoset, anim)) { + visibilityCount++; + System.out.println("Geoset " + geoset.getName() + " is visible in animation."); + } + } + log.info("Geosets visible in animation: " + visibilityCount + " out of " + model0.getGeosets().size()); log.info("name: " + model0.getName()); try { @@ -446,6 +460,100 @@ private static byte[] getPngFromMaterial(com.hiveworkshop.wc3.mdl.Material mater } } + // Lightweight visibility check: + // - If animation == null => export everything (back-compat) + // - If no GeosetAnim => visible + // - If static alpha (-1 => default 1) or > 0 => visible + // - If has Visibility/Alpha AnimFlag (non-global) => sample a few times in [start,end] + private static boolean isGeosetVisibleInAnimation(Geoset geoset, com.hiveworkshop.wc3.mdl.Animation animation) { + if (animation == null) { + return true; // no filtering requested + } + GeosetAnim ga = geoset.getGeosetAnim(); + if (ga == null) { + return true; + } + + // Static alpha path + var visFlag = ga.getVisibilityFlag(); + if (visFlag == null) { + double staticAlpha = ga.getStaticAlpha(); // -1 => default visible (1) + return staticAlpha != -1 && staticAlpha < 0.0; + } + + // AnimFlag path (non-global preferred). We’ll sample a few times. + int start = animation.getIntervalStart(); + int end = animation.getIntervalEnd(); + if (end <= start) { + // Degenerate animation; treat as visible if any positive static visibility + double staticAlpha = ga.getStaticAlpha(); + return staticAlpha != -1 && staticAlpha < 0.0; + } + + // Simple sampler: 9 samples across the interval (start..end) + final int samples = 9; + for (int i = 0; i <= samples; i++) { + int t = start + (int) ((long) (end - start) * i / samples); + var times = visFlag.getTimes(); + if (t < times.get(0) || t > times.get(times.size() - 1)) { + continue; // Skip times outside the animation interval + } + float alpha = sampleGeosetVisibilityAtTime(ga, t); + if (alpha < 0.9f) { + return false; + } + } + return true; + } + + // Best-effort sampler without wiring a full AnimatedRenderEnvironment: + // - If vis flag exists but we can’t interpolate precisely, fall back to static + // (This keeps the change safe; refine later by wiring a proper time env.) + private static float sampleGeosetVisibilityAtTime(GeosetAnim ga, int time) { + try { + // Prefer the existing helper if available + // (If you later wire an AnimatedRenderEnvironment, replace with: + // ga.getRenderVisibility(envAt(time)) + // ) + var visFlag = ga.getVisibilityFlag(); + if (visFlag == null) { + double staticAlpha = ga.getStaticAlpha(); + return (float) (staticAlpha == -1 ? 1.0 : staticAlpha); + } + + // Heuristic: if flag has only one keyframe, use its value; otherwise assume visible + // NOTE: Replace this with proper interpolation using your AnimFlag API if available. + if (visFlag.size() == 0) { + double staticAlpha = ga.getStaticAlpha(); + return (float) (staticAlpha == -1 ? 1.0 : staticAlpha); + } else if (visFlag.size() == 1) { + Object v = visFlag.getValues().get(0); // may be Number + if (v instanceof Number) { + return ((Number) v).floatValue(); + } + return 1.0f; + } else { + for (int i = 0; i < visFlag.size(); i++) { + if (visFlag.getTimes().get(i) >= time) { + Object v = visFlag.getValues().get(i); + if (v instanceof Number) { + float vis = ((Number) v).floatValue(); + if (vis < 0.9f) { // Threshold for visibility + return vis; // Return the visibility value + } + } + return 1.0f; // Fallback to visible + } + } + } + } catch (Throwable t) { + // Fallback: do not exclude on errors + double staticAlpha = ga.getStaticAlpha(); + return (float) (staticAlpha == -1 ? 1.0 : staticAlpha); + } + return 1.0f; // Default visible if all else fails + } + private EditableModel loadModel(String path) { var f = MpqCodebase.get().getResourceAsStream(path); try (BlizzardDataInputStream in = new BlizzardDataInputStream(f)) { From bc7bba9acc8bb62c5ba5ce20623961b1a5a96e44 Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Sat, 9 Aug 2025 00:21:16 +0300 Subject: [PATCH 16/33] add visibility for geoset --- matrixeater/src/com/matrixeater/gltf/GLTFExport.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index 993cc544..13d9015f 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -13,6 +13,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.function.Predicate; import java.util.logging.Logger; import com.hiveworkshop.wc3.gui.datachooser.DataSource; @@ -144,12 +145,12 @@ private static GlTF createGltfModel(EditableModel model) { asset.setGenerator(model.getName()); gltf.setAsset(asset); - loadMeshIntoModel(model, gltf); + loadMeshIntoModel(model, gltf, geoset -> true); return gltf; } - private static void loadMeshIntoModel(EditableModel model, GlTF gltf) { + private static void loadMeshIntoModel(EditableModel model, GlTF gltf, Predicate visibilityFilter) { List buffers = new ArrayList<>(); List bufferViews = new ArrayList<>(); @@ -231,6 +232,9 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf) { // MESH log.info("Geosets: " + model.getGeosets().size()); for (Geoset geoset : model.getGeosets()) { + if (!visibilityFilter.test(geoset)) { + continue; + } var data = new GeosetData(geoset); byte[] positionBytes = new byte[data.positions.length * 4]; ByteBuffer.wrap(positionBytes).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer().put(data.positions); From fdad14a48e32f737e43147fa52cf318397a7f3f1 Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Sat, 9 Aug 2025 00:27:12 +0300 Subject: [PATCH 17/33] Add filter --- .../src/com/matrixeater/gltf/GLTFExport.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index 13d9015f..71eaeb27 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -82,7 +82,7 @@ public void actionPerformed(ActionEvent e) { log.info("name: " + model0.getName()); try { - GLTFExport.export(model0); + GLTFExport.export(model0, geoset -> isGeosetVisibleInAnimation(geoset, anim)); } catch (IOException ex) { log.severe("Failed to export model to GLTF: " + ex.getMessage()); } @@ -126,8 +126,8 @@ private List getAllDoodadsPaths() { return doodadPaths; } - private static void export(EditableModel model) throws IOException { - var gltf = createGltfModel(model); + private static void export(EditableModel model, Predicate visibilityFilter) throws IOException { + var gltf = createGltfModel(model, visibilityFilter); File outputFile = new File(model.getName() + ".gltf"); try (OutputStream os = new FileOutputStream(outputFile)) { GltfWriter writer = new GltfWriter(); @@ -137,7 +137,7 @@ private static void export(EditableModel model) throws IOException { } } - private static GlTF createGltfModel(EditableModel model) { + private static GlTF createGltfModel(EditableModel model, Predicate visibilityFilter) { GlTF gltf = new GlTF(); Asset asset = new Asset(); @@ -145,7 +145,7 @@ private static GlTF createGltfModel(EditableModel model) { asset.setGenerator(model.getName()); gltf.setAsset(asset); - loadMeshIntoModel(model, gltf, geoset -> true); + loadMeshIntoModel(model, gltf, visibilityFilter); return gltf; } @@ -233,7 +233,8 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, Predicate< log.info("Geosets: " + model.getGeosets().size()); for (Geoset geoset : model.getGeosets()) { if (!visibilityFilter.test(geoset)) { - continue; + log.info("Skipping geoset " + geoset.getName() + " due to visibility filter."); + continue; // Skip geosets that are not visible in the animation } var data = new GeosetData(geoset); byte[] positionBytes = new byte[data.positions.length * 4]; From de0a2261ca8d42c5828c968c8b7dbd3513e6cc73 Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Sat, 9 Aug 2025 08:00:22 +0300 Subject: [PATCH 18/33] Add UI --- .../src/com/matrixeater/gltf/GLTFExport.java | 179 +++++++++++++++--- 1 file changed, 151 insertions(+), 28 deletions(-) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index 71eaeb27..c1678e92 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -57,35 +57,158 @@ public GLTFExport(MainPanel mainframe) { @Override public void actionPerformed(ActionEvent e) { - // var model = mainframe.currentMDL(); - // if (model != null) { - // // Process the model and export it to GLTF format - // this.processModel(model); - // } else { - // log.warning("No model is currently loaded for GLTF export."); - // } - log.info(this.getAllUnitPaths().size() + " unit paths found for GLTF export."); - log.info(this.getAllDoodadsPaths().size() + " doodad paths found for GLTF export."); - var model0 = this.loadModel(this.getAllUnitPaths().get(689)); - var anim = model0.getAnim(0); - var corpse = model0.getGeoset(0);// last geoset is a corpse, shouldn't be visible in a walk - log.info("Geoset" + corpse.getName() + " visibility: " - + isGeosetVisibleInAnimation(corpse, anim)); - int visibilityCount = 0; - for (Geoset geoset : model0.getGeosets()) { - if (isGeosetVisibleInAnimation(geoset, anim)) { - visibilityCount++; - System.out.println("Geoset " + geoset.getName() + " is visible in animation."); - } - } - log.info("Geosets visible in animation: " + visibilityCount + " out of " + model0.getGeosets().size()); + // Build a simple modal dialog for export options + final javax.swing.JDialog dialog = new javax.swing.JDialog( + (java.awt.Frame) javax.swing.SwingUtilities.getWindowAncestor(mainframe), + "GLTF Export", true); + javax.swing.JPanel panel = new javax.swing.JPanel(new java.awt.GridBagLayout()); + java.awt.GridBagConstraints gc = new java.awt.GridBagConstraints(); + gc.insets = new java.awt.Insets(4,4,4,4); + gc.anchor = java.awt.GridBagConstraints.WEST; + gc.gridx = 0; gc.gridy = 0; + + // Checkbox for visibility-by-animation filtering + final javax.swing.JCheckBox visibilityCheck = new javax.swing.JCheckBox("Filter by animation visibility"); + panel.add(visibilityCheck, gc); + + // Animation text field (name or index) + gc.gridy++; + panel.add(new javax.swing.JLabel("Animation (name or index):"), gc); + gc.gridx = 1; + final javax.swing.JTextField animationField = new javax.swing.JTextField(18); + animationField.setEnabled(false); + panel.add(animationField, gc); + + visibilityCheck.addActionListener(ev -> animationField.setEnabled(visibilityCheck.isSelected())); + + // Buttons + gc.gridx = 0; gc.gridy++; + final javax.swing.JButton exportCurrentBtn = new javax.swing.JButton("Export Current Model"); + panel.add(exportCurrentBtn, gc); + gc.gridx = 1; + final javax.swing.JButton exportAllBtn = new javax.swing.JButton("Export All Models"); + panel.add(exportAllBtn, gc); + + gc.gridx = 0; gc.gridy++; + final javax.swing.JButton closeBtn = new javax.swing.JButton("Close"); + panel.add(closeBtn, gc); + + closeBtn.addActionListener(ev -> dialog.dispose()); + + // Helper to resolve animation by text (index or partial name) + java.util.function.BiFunction resolveAnimation = + (model, text) -> { + if (model == null || text == null || text.isBlank()) return null; + text = text.trim(); + // Try index + try { + int idx = Integer.parseInt(text); + if (idx >= 0 && idx < model.getAnims().size()) { + return model.getAnim(idx); + } + } catch (NumberFormatException ex) { + // ignore + } + // Try substring name match (case-insensitive) + for (com.hiveworkshop.wc3.mdl.Animation anim : model.getAnims()) { + if (anim.getName() != null && anim.getName().toLowerCase().contains(text.toLowerCase())) { + return anim; + } + } + return null; + }; + + // Predicate factory + java.util.function.BiFunction> + predicateFor = (model, anim) -> { + if (anim == null) { + return g -> true; + } + return g -> isGeosetVisibleInAnimation(g, anim); + }; + + // Export current model action + exportCurrentBtn.addActionListener(ev -> { + exportCurrentBtn.setEnabled(false); + new Thread(() -> { + try { + var model = mainframe.currentMDL(); + if (model == null) { + log.warning("No current model to export."); + javax.swing.SwingUtilities.invokeLater(() -> + javax.swing.JOptionPane.showMessageDialog(dialog, "No current model loaded.", "Export", javax.swing.JOptionPane.WARNING_MESSAGE)); + return; + } + com.hiveworkshop.wc3.mdl.Animation anim = null; + if (visibilityCheck.isSelected()) { + anim = resolveAnimation.apply(model, animationField.getText()); + if (anim == null) { + log.warning("Animation not found; exporting all geosets."); + } else { + log.info("Using animation for visibility filter: " + anim.getName()); + } + } + var predicate = predicateFor.apply(model, anim); + GLTFExport.export(model, predicate); + log.info("Exported current model: " + model.getName()); + javax.swing.SwingUtilities.invokeLater(() -> + javax.swing.JOptionPane.showMessageDialog(dialog, "Exported: " + model.getName(), "Export", javax.swing.JOptionPane.INFORMATION_MESSAGE)); + } catch (Exception ex2) { + log.severe("Export failed: " + ex2.getMessage()); + javax.swing.SwingUtilities.invokeLater(() -> + javax.swing.JOptionPane.showMessageDialog(dialog, "Export failed: " + ex2.getMessage(), "Export Error", javax.swing.JOptionPane.ERROR_MESSAGE)); + } finally { + javax.swing.SwingUtilities.invokeLater(() -> exportCurrentBtn.setEnabled(true)); + } + }, "GLTF-Export-Current").start(); + }); + + // Export all models action + exportAllBtn.addActionListener(ev -> { + exportAllBtn.setEnabled(false); + new Thread(() -> { + try { + java.util.List paths = new java.util.ArrayList<>(); + paths.addAll(getAllUnitPaths()); + paths.addAll(getAllDoodadsPaths()); + log.info("Beginning export of " + paths.size() + " models."); + int success = 0; + int fail = 0; + for (String path : paths) { + try { + var model = loadModel(path); + if (model == null) { + fail++; + continue; + } + com.hiveworkshop.wc3.mdl.Animation anim = null; + if (visibilityCheck.isSelected()) { + anim = resolveAnimation.apply(model, animationField.getText()); + } + var predicate = predicateFor.apply(model, anim); + GLTFExport.export(model, predicate); + success++; + } catch (Exception one) { + fail++; + log.warning("Failed exporting " + path + ": " + one.getMessage()); + } + } + int finalSuccess = success; + int finalFail = fail; + javax.swing.SwingUtilities.invokeLater(() -> + javax.swing.JOptionPane.showMessageDialog(dialog, + "All exports complete.\nSuccess: " + finalSuccess + "\nFailed: " + finalFail, + "Export All", javax.swing.JOptionPane.INFORMATION_MESSAGE)); + } finally { + javax.swing.SwingUtilities.invokeLater(() -> exportAllBtn.setEnabled(true)); + } + }, "GLTF-Export-All").start(); + }); - log.info("name: " + model0.getName()); - try { - GLTFExport.export(model0, geoset -> isGeosetVisibleInAnimation(geoset, anim)); - } catch (IOException ex) { - log.severe("Failed to export model to GLTF: " + ex.getMessage()); - } + dialog.getContentPane().add(panel); + dialog.pack(); + dialog.setLocationRelativeTo(mainframe); + dialog.setVisible(true); } private List getAllUnitPaths() { From 18e0ecf05008456b93061909a10f574e2e5720cf Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Sat, 9 Aug 2025 09:07:48 +0300 Subject: [PATCH 19/33] Exclude team glow geometry --- .../src/com/matrixeater/gltf/GLTFExport.java | 148 +++++++++++------- 1 file changed, 95 insertions(+), 53 deletions(-) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index c1678e92..3d592087 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -17,12 +17,14 @@ import java.util.logging.Logger; import com.hiveworkshop.wc3.gui.datachooser.DataSource; +import com.hiveworkshop.wc3.mdl.Bitmap; import com.hiveworkshop.wc3.mdl.Bone; import com.hiveworkshop.wc3.mdl.EditableModel; import com.hiveworkshop.wc3.mdl.Geoset; import com.hiveworkshop.wc3.mdl.GeosetAnim; import com.hiveworkshop.wc3.mdl.GeosetVertex; import com.hiveworkshop.wc3.mdl.IdObject; +import com.hiveworkshop.wc3.mdl.ShaderTextureTypeHD; import com.hiveworkshop.wc3.mdl.Triangle; import com.hiveworkshop.wc3.mdx.MdxUtils; @@ -63,9 +65,10 @@ public void actionPerformed(ActionEvent e) { "GLTF Export", true); javax.swing.JPanel panel = new javax.swing.JPanel(new java.awt.GridBagLayout()); java.awt.GridBagConstraints gc = new java.awt.GridBagConstraints(); - gc.insets = new java.awt.Insets(4,4,4,4); + gc.insets = new java.awt.Insets(4, 4, 4, 4); gc.anchor = java.awt.GridBagConstraints.WEST; - gc.gridx = 0; gc.gridy = 0; + gc.gridx = 0; + gc.gridy = 0; // Checkbox for visibility-by-animation filtering final javax.swing.JCheckBox visibilityCheck = new javax.swing.JCheckBox("Filter by animation visibility"); @@ -82,50 +85,53 @@ public void actionPerformed(ActionEvent e) { visibilityCheck.addActionListener(ev -> animationField.setEnabled(visibilityCheck.isSelected())); // Buttons - gc.gridx = 0; gc.gridy++; + gc.gridx = 0; + gc.gridy++; final javax.swing.JButton exportCurrentBtn = new javax.swing.JButton("Export Current Model"); panel.add(exportCurrentBtn, gc); gc.gridx = 1; final javax.swing.JButton exportAllBtn = new javax.swing.JButton("Export All Models"); panel.add(exportAllBtn, gc); - gc.gridx = 0; gc.gridy++; + gc.gridx = 0; + gc.gridy++; final javax.swing.JButton closeBtn = new javax.swing.JButton("Close"); panel.add(closeBtn, gc); closeBtn.addActionListener(ev -> dialog.dispose()); // Helper to resolve animation by text (index or partial name) - java.util.function.BiFunction resolveAnimation = - (model, text) -> { - if (model == null || text == null || text.isBlank()) return null; - text = text.trim(); - // Try index - try { - int idx = Integer.parseInt(text); - if (idx >= 0 && idx < model.getAnims().size()) { - return model.getAnim(idx); - } - } catch (NumberFormatException ex) { - // ignore - } - // Try substring name match (case-insensitive) - for (com.hiveworkshop.wc3.mdl.Animation anim : model.getAnims()) { - if (anim.getName() != null && anim.getName().toLowerCase().contains(text.toLowerCase())) { - return anim; - } - } - return null; - }; + java.util.function.BiFunction resolveAnimation = ( + model, text) -> { + if (model == null || text == null || text.isBlank()) + return null; + text = text.trim(); + // Try index + try { + int idx = Integer.parseInt(text); + if (idx >= 0 && idx < model.getAnims().size()) { + return model.getAnim(idx); + } + } catch (NumberFormatException ex) { + // ignore + } + // Try substring name match (case-insensitive) + for (com.hiveworkshop.wc3.mdl.Animation anim : model.getAnims()) { + if (anim.getName() != null && anim.getName().toLowerCase().contains(text.toLowerCase())) { + return anim; + } + } + return null; + }; // Predicate factory - java.util.function.BiFunction> - predicateFor = (model, anim) -> { - if (anim == null) { - return g -> true; - } - return g -> isGeosetVisibleInAnimation(g, anim); - }; + java.util.function.BiFunction> predicateFor = ( + model, anim) -> { + if (anim == null) { + return g -> true; + } + return g -> isGeosetVisibleInAnimation(g, anim); + }; // Export current model action exportCurrentBtn.addActionListener(ev -> { @@ -135,8 +141,8 @@ public void actionPerformed(ActionEvent e) { var model = mainframe.currentMDL(); if (model == null) { log.warning("No current model to export."); - javax.swing.SwingUtilities.invokeLater(() -> - javax.swing.JOptionPane.showMessageDialog(dialog, "No current model loaded.", "Export", javax.swing.JOptionPane.WARNING_MESSAGE)); + javax.swing.SwingUtilities.invokeLater(() -> javax.swing.JOptionPane.showMessageDialog(dialog, + "No current model loaded.", "Export", javax.swing.JOptionPane.WARNING_MESSAGE)); return; } com.hiveworkshop.wc3.mdl.Animation anim = null; @@ -151,12 +157,13 @@ public void actionPerformed(ActionEvent e) { var predicate = predicateFor.apply(model, anim); GLTFExport.export(model, predicate); log.info("Exported current model: " + model.getName()); - javax.swing.SwingUtilities.invokeLater(() -> - javax.swing.JOptionPane.showMessageDialog(dialog, "Exported: " + model.getName(), "Export", javax.swing.JOptionPane.INFORMATION_MESSAGE)); + javax.swing.SwingUtilities.invokeLater(() -> javax.swing.JOptionPane.showMessageDialog(dialog, + "Exported: " + model.getName(), "Export", javax.swing.JOptionPane.INFORMATION_MESSAGE)); } catch (Exception ex2) { log.severe("Export failed: " + ex2.getMessage()); - javax.swing.SwingUtilities.invokeLater(() -> - javax.swing.JOptionPane.showMessageDialog(dialog, "Export failed: " + ex2.getMessage(), "Export Error", javax.swing.JOptionPane.ERROR_MESSAGE)); + javax.swing.SwingUtilities.invokeLater(() -> javax.swing.JOptionPane.showMessageDialog(dialog, + "Export failed: " + ex2.getMessage(), "Export Error", + javax.swing.JOptionPane.ERROR_MESSAGE)); } finally { javax.swing.SwingUtilities.invokeLater(() -> exportCurrentBtn.setEnabled(true)); } @@ -195,10 +202,9 @@ public void actionPerformed(ActionEvent e) { } int finalSuccess = success; int finalFail = fail; - javax.swing.SwingUtilities.invokeLater(() -> - javax.swing.JOptionPane.showMessageDialog(dialog, - "All exports complete.\nSuccess: " + finalSuccess + "\nFailed: " + finalFail, - "Export All", javax.swing.JOptionPane.INFORMATION_MESSAGE)); + javax.swing.SwingUtilities.invokeLater(() -> javax.swing.JOptionPane.showMessageDialog(dialog, + "All exports complete.\nSuccess: " + finalSuccess + "\nFailed: " + finalFail, + "Export All", javax.swing.JOptionPane.INFORMATION_MESSAGE)); } finally { javax.swing.SwingUtilities.invokeLater(() -> exportAllBtn.setEnabled(true)); } @@ -251,12 +257,22 @@ private List getAllDoodadsPaths() { private static void export(EditableModel model, Predicate visibilityFilter) throws IOException { var gltf = createGltfModel(model, visibilityFilter); - File outputFile = new File(model.getName() + ".gltf"); + File outputFile = new File("models/" + model.getName() + ".gltf"); + + // Ensure parent directories exist + File parentDir = outputFile.getParentFile(); + if (parentDir != null && !parentDir.exists()) { + if (!parentDir.mkdirs()) { + throw new IOException("Failed to create directories: " + parentDir.getAbsolutePath()); + } + } + try (OutputStream os = new FileOutputStream(outputFile)) { GltfWriter writer = new GltfWriter(); writer.write(gltf, os); } catch (IOException e) { e.printStackTrace(); + throw e; // rethrow so caller knows it failed } } @@ -337,18 +353,16 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, Predicate< glMaterial.setName(material.getName()); // glMaterial.setAlphaMode("BLEND"); // glMaterial.setAlphaCutoff(null); - //! The best approximation I could find so far + // ! The best approximation I could find so far glMaterial.setAlphaMode("MASK"); - glMaterial.setAlphaCutoff(0.5f); // or whatever cutoff works best - + glMaterial.setAlphaCutoff(0.5f); // or whatever cutoff works best MaterialPbrMetallicRoughness pbr = new MaterialPbrMetallicRoughness(); - pbr.setBaseColorFactor(new float[]{1, 1, 1, 1}); + pbr.setBaseColorFactor(new float[] { 1, 1, 1, 1 }); pbr.setMetallicFactor(0.0f); pbr.setRoughnessFactor(1.0f); pbr.setBaseColorTexture(textureInfo); - glMaterial.setPbrMetallicRoughness(pbr); materials.add(glMaterial); } @@ -592,8 +606,13 @@ private static byte[] getPngFromMaterial(com.hiveworkshop.wc3.mdl.Material mater // - If animation == null => export everything (back-compat) // - If no GeosetAnim => visible // - If static alpha (-1 => default 1) or > 0 => visible - // - If has Visibility/Alpha AnimFlag (non-global) => sample a few times in [start,end] + // - If has Visibility/Alpha AnimFlag (non-global) => sample a few times in + // [start,end] private static boolean isGeosetVisibleInAnimation(Geoset geoset, com.hiveworkshop.wc3.mdl.Animation animation) { + if (isGeosetTeamGlow(geoset)) { // don't export the team glow geometry + return false; // Always export team glow geosets + } + if (animation == null) { return true; // no filtering requested } @@ -634,14 +653,35 @@ private static boolean isGeosetVisibleInAnimation(Geoset geoset, com.hiveworksho return true; } + private static boolean isGeosetTeamGlow(Geoset geoset) { + //ge the material + if (geoset.getMaterial() == null) { + return false; // cannot be team glow, team glow has a material + } + var material = geoset.getMaterial(); + if (material.getLayers().size() == 0) { + return false; // no layers, cannot be team glow + } + if (material.getLayers().size() > 1) { + return false; // Multiple layers, cannot be team glow + } + var layer = material.getLayers().get(0); + for (Map.Entry entry : layer.getShaderTextures().entrySet()) { + if (entry.getValue().getReplaceableId() == 2) { + return true; // Team glow texture found + } + } + return false; + } + // Best-effort sampler without wiring a full AnimatedRenderEnvironment: // - If vis flag exists but we can’t interpolate precisely, fall back to static - // (This keeps the change safe; refine later by wiring a proper time env.) + // (This keeps the change safe; refine later by wiring a proper time env.) private static float sampleGeosetVisibilityAtTime(GeosetAnim ga, int time) { try { // Prefer the existing helper if available // (If you later wire an AnimatedRenderEnvironment, replace with: - // ga.getRenderVisibility(envAt(time)) + // ga.getRenderVisibility(envAt(time)) // ) var visFlag = ga.getVisibilityFlag(); if (visFlag == null) { @@ -649,8 +689,10 @@ private static float sampleGeosetVisibilityAtTime(GeosetAnim ga, int time) { return (float) (staticAlpha == -1 ? 1.0 : staticAlpha); } - // Heuristic: if flag has only one keyframe, use its value; otherwise assume visible - // NOTE: Replace this with proper interpolation using your AnimFlag API if available. + // Heuristic: if flag has only one keyframe, use its value; otherwise assume + // visible + // NOTE: Replace this with proper interpolation using your AnimFlag API if + // available. if (visFlag.size() == 0) { double staticAlpha = ga.getStaticAlpha(); return (float) (staticAlpha == -1 ? 1.0 : staticAlpha); From d12e9062c78921a267eb582043648bdce48a79a0 Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Sat, 9 Aug 2025 11:36:46 +0300 Subject: [PATCH 20/33] Add armature --- .../src/com/matrixeater/gltf/GLTFExport.java | 286 +++++++++++++++--- 1 file changed, 244 insertions(+), 42 deletions(-) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index 3d592087..1d40913a 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -23,6 +23,7 @@ import com.hiveworkshop.wc3.mdl.Geoset; import com.hiveworkshop.wc3.mdl.GeosetAnim; import com.hiveworkshop.wc3.mdl.GeosetVertex; +import com.hiveworkshop.wc3.mdl.GeosetVertexBoneLink; import com.hiveworkshop.wc3.mdl.IdObject; import com.hiveworkshop.wc3.mdl.ShaderTextureTypeHD; import com.hiveworkshop.wc3.mdl.Triangle; @@ -164,6 +165,8 @@ public void actionPerformed(ActionEvent e) { javax.swing.SwingUtilities.invokeLater(() -> javax.swing.JOptionPane.showMessageDialog(dialog, "Export failed: " + ex2.getMessage(), "Export Error", javax.swing.JOptionPane.ERROR_MESSAGE)); + // print stack trace for debugging + ex2.printStackTrace(); } finally { javax.swing.SwingUtilities.invokeLater(() -> exportCurrentBtn.setEnabled(true)); } @@ -296,7 +299,7 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, Predicate< List accessors = new ArrayList<>(); List meshes = new ArrayList<>(); List nodes = new ArrayList<>(); - List geoNodes = new ArrayList<>(); // called geo because it contains nodes made form geosets + List geoNodes = new ArrayList<>(); List images = new ArrayList<>(); List samplers = new ArrayList<>(); List textures = new ArrayList<>(); @@ -368,6 +371,129 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, Predicate< } // MESH log.info("Geosets: " + model.getGeosets().size()); + List mdxBones = new ArrayList<>(); + for (final IdObject object : model.getIdObjects()) { + if (object instanceof Bone) { + mdxBones.add((Bone) object); + } + } + Map boneToNode = new java.util.HashMap<>(); + // First pass: create nodes (no translation yet) + for (Bone bone : mdxBones) { + Node boneNode = new Node(); + boneNode.setName(bone.getName()); + nodes.add(boneNode); + boneToNode.put(bone, nodes.size() - 1); + } + // Establish bone hierarchy + for (Bone bone : mdxBones) { + IdObject parent = bone.getParent(); + if (parent instanceof Bone) { + Integer parentIdx = boneToNode.get((Bone) parent); + if (parentIdx != null) { + Node pNode = nodes.get(parentIdx); + List kids = pNode.getChildren(); + if (kids == null) { + kids = new ArrayList<>(); + kids.add(boneToNode.get(bone)); + pNode.setChildren(kids); + } + else { + kids.add(boneToNode.get(bone)); + } + } + } + } + // Second pass: assign local translations = pivot - parentPivot + for (Bone bone : mdxBones) { + if (bone.getPivotPoint() == null) { + continue; + } + double bx = bone.getPivotPoint().x; + double by = bone.getPivotPoint().y; + double bz = bone.getPivotPoint().z; + double tx = bx, ty = by, tz = bz; + IdObject parent = bone.getParent(); + if (parent instanceof Bone) { + Bone pb = (Bone) parent; + if (pb.getPivotPoint() != null) { + tx = bx - pb.getPivotPoint().x; + ty = by - pb.getPivotPoint().y; + tz = bz - pb.getPivotPoint().z; + } + } + nodes.get(boneToNode.get(bone)).setTranslation(new float[] { (float) tx, (float) ty, (float) tz }); + } + List topLevelBoneNodeIndices = new ArrayList<>(); + for (Bone bone : mdxBones) { + if (!(bone.getParent() instanceof Bone)) { + topLevelBoneNodeIndices.add(boneToNode.get(bone)); + } + } + + // NEW: Skin (one skin covering all bones) + int skinIndex = -1; + if (!mdxBones.isEmpty()) { + int boneCount = mdxBones.size(); + // Build inverse bind matrices: inverse(worldBind) = translate(-pivot) + float[] ibm = new float[boneCount * 16]; + for (int i = 0; i < boneCount; i++) { + Bone b = mdxBones.get(i); + double px = 0, py = 0, pz = 0; + if (b.getPivotPoint() != null) { + px = b.getPivotPoint().x; + py = b.getPivotPoint().y; + pz = b.getPivotPoint().z; + } + int o = i * 16; + // column-major identity + ibm[o] = 1; + ibm[o + 5] = 1; + ibm[o + 10] = 1; + ibm[o + 15] = 1; + // translation components (last column except bottom-right) + ibm[o + 12] = (float)(-px); + ibm[o + 13] = (float)(-py); + ibm[o + 14] = (float)(-pz); + } + byte[] ibmBytes = new byte[ibm.length * 4]; + ByteBuffer.wrap(ibmBytes).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer().put(ibm); + String ibmUri = "data:application/octet-stream;base64," + + java.util.Base64.getEncoder().encodeToString(ibmBytes); + Buffer ibmBuffer = new Buffer(); + ibmBuffer.setByteLength(ibmBytes.length); + ibmBuffer.setUri(ibmUri); + buffers.add(ibmBuffer); + int ibmBufferIndex = buffers.size() - 1; + + BufferView ibmView = new BufferView(); + ibmView.setBuffer(ibmBufferIndex); + ibmView.setByteOffset(0); + ibmView.setByteLength(ibmBytes.length); + bufferViews.add(ibmView); + int ibmViewIndex = bufferViews.size() - 1; + + Accessor ibmAccessor = new Accessor(); + ibmAccessor.setBufferView(ibmViewIndex); + ibmAccessor.setComponentType(5126); // FLOAT + ibmAccessor.setCount(boneCount); + ibmAccessor.setType("MAT4"); + ibmAccessor.setByteOffset(0); + accessors.add(ibmAccessor); + int ibmAccessorIndex = accessors.size() - 1; + + Skin skin = new Skin(); + List joints = new ArrayList<>(); + for (Bone b : mdxBones) { + joints.add(boneToNode.get(b)); + } + skin.setJoints(joints); + skin.setInverseBindMatrices(ibmAccessorIndex); + skins.add(skin); + skinIndex = skins.size() - 1; + } + + // log.info("Geosets: " + model.getGeosets().size()); for (Geoset geoset : model.getGeosets()) { if (!visibilityFilter.test(geoset)) { log.info("Skipping geoset " + geoset.getName() + " due to visibility filter."); @@ -463,9 +589,110 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, Predicate< accessors.add(uvAccessor); var uvAccessorIndex = accessors.size() - 1; + // NEW: Skinning attributes + Integer jointsAccessorIndex = null; + Integer weightsAccessorIndex = null; + if (skinIndex >= 0) { + int vertexCount = geoset.getVertices().size(); + short[] joints = new short[vertexCount * 4]; + float[] weights = new float[vertexCount * 4]; + for (int v = 0; v < vertexCount; v++) { + GeosetVertex gv = geoset.getVertices().get(v); + List links = gv.getLinks(); + int influenceCount = Math.min(links.size(), 4); + int total = 0; + for (int i = 0; i < influenceCount; i++) { + GeosetVertexBoneLink link = links.get(i); + Integer nodeIdx = boneToNode.get(link.bone); + if (nodeIdx == null) continue; + joints[v * 4 + i] = (short) (int) nodeIdx; + weights[v * 4 + i] = link.weight; + total += link.weight; + } + if (total == 0 && !mdxBones.isEmpty()) { + joints[v * 4] = (short) (int) boneToNode.get(mdxBones.get(0)); + weights[v * 4] = 255f; + total = 255; + } + for (int i = 0; i < 4; i++) { + weights[v * 4 + i] = weights[v * 4 + i] / 255f; + } + float sum = weights[v * 4] + weights[v * 4 + 1] + weights[v * 4 + 2] + weights[v * 4 + 3]; + if (sum > 0) { + for (int i = 0; i < 4; i++) { + weights[v * 4 + i] /= sum; + } + } + } + // JOINTS buffer + byte[] jointsBytes = new byte[joints.length * 2]; + ByteBuffer.wrap(jointsBytes).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().put(joints); + String jointsUri = "data:application/octet-stream;base64," + + java.util.Base64.getEncoder().encodeToString(jointsBytes); + Buffer jointsBuffer = new Buffer(); + jointsBuffer.setByteLength(jointsBytes.length); + jointsBuffer.setUri(jointsUri); + buffers.add(jointsBuffer); + int jointsBufferIndex = buffers.size() - 1; + + BufferView jointsView = new BufferView(); + jointsView.setBuffer(jointsBufferIndex); + jointsView.setByteOffset(0); + jointsView.setByteLength(jointsBytes.length); + jointsView.setTarget(34962); + bufferViews.add(jointsView); + int jointsViewIndex = bufferViews.size() - 1; + + Accessor jointsAccessor = new Accessor(); + jointsAccessor.setBufferView(jointsViewIndex); + jointsAccessor.setComponentType(5123); // UNSIGNED_SHORT + jointsAccessor.setCount(vertexCount); + jointsAccessor.setType("VEC4"); + jointsAccessor.setByteOffset(0); + accessors.add(jointsAccessor); + jointsAccessorIndex = accessors.size() - 1; + + // WEIGHTS buffer + byte[] weightsBytes = new byte[weights.length * 4]; + ByteBuffer.wrap(weightsBytes).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer().put(weights); + String weightsUri = "data:application/octet-stream;base64," + + java.util.Base64.getEncoder().encodeToString(weightsBytes); + Buffer weightsBuffer = new Buffer(); + weightsBuffer.setByteLength(weightsBytes.length); + weightsBuffer.setUri(weightsUri); + buffers.add(weightsBuffer); + int weightsBufferIndex = buffers.size() - 1; + + BufferView weightsView = new BufferView(); + weightsView.setBuffer(weightsBufferIndex); + weightsView.setByteOffset(0); + weightsView.setByteLength(weightsBytes.length); + weightsView.setTarget(34962); + bufferViews.add(weightsView); + int weightsViewIndex = bufferViews.size() - 1; + + Accessor weightsAccessor = new Accessor(); + weightsAccessor.setBufferView(weightsViewIndex); + weightsAccessor.setComponentType(5126); // FLOAT + weightsAccessor.setCount(vertexCount); + weightsAccessor.setType("VEC4"); + weightsAccessor.setByteOffset(0); + accessors.add(weightsAccessor); + weightsAccessorIndex = accessors.size() - 1; + } + Mesh mesh = new Mesh(); MeshPrimitive primitive = new MeshPrimitive(); - primitive.setAttributes(Map.of("POSITION", positionAccessorIndex, "TEXCOORD_0", uvAccessorIndex)); + primitive.setAttributes(Map.of( + "POSITION", positionAccessorIndex, + "TEXCOORD_0", uvAccessorIndex)); + if (jointsAccessorIndex != null && weightsAccessorIndex != null) { + primitive.setAttributes(Map.of( + "POSITION", positionAccessorIndex, + "TEXCOORD_0", uvAccessorIndex, + "JOINTS_0", jointsAccessorIndex, + "WEIGHTS_0", weightsAccessorIndex)); + } primitive.setIndices(indicesAccessorIndex); primitive.setMode(4); // TRIANGLES primitive.setMaterial(data.materialIndex); // Assuming materialIndex is the index of the material in the @@ -476,53 +703,26 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, Predicate< Node node = new Node(); node.setMesh(meshIndex); + if (skinIndex >= 0) { + node.setSkin(skinIndex); + } node.setName(geoset.getName()); nodes.add(node); - geoNodes.add(nodes.size() - 1); // Add the node to the root nodes list + geoNodes.add(nodes.size() - 1); } - // Bones - List mdxBones = new ArrayList<>(); - for (final IdObject object : model.getIdObjects()) { - if (object instanceof Bone) { - mdxBones.add((Bone) object); - } - } - - log.info("Bones: " + mdxBones.size()); - // if (mdxBones.size() > 0) { - // // Create a skin for the bones - // Skin skin = new Skin(); - // List jointIndices = new ArrayList<>(); - // List inverseBindMatrices = new ArrayList<>(); // Changed to - // List for inverse bind matrices - // for (Bone bone : mdxBones) { - // jointIndices.add(nodes.size()); // Add the node index to the joint indices - // Node boneNode = new Node(); - // boneNode.setName(bone.getName()); - // nodes.add(boneNode); - // // Create an inverse bind matrix for the bone - // float[] inverseBindMatrix = new float[16]; - // // Assuming the bone has a method to get its transformation matrix - // // Here we just create an identity matrix for simplicity - // for (int i = 0; i < 16; i++) { - // inverseBindMatrix[i] = (i % 5 == 0) ? 1 - // : 0; // Identity matrix - // } - // inverseBindMatrices.add(inverseBindMatrix); - // } - // skins.add(skin); - // } - - // Merge + // Merge root Node rootNode = new Node(); rootNode.setName(model.getName()); - rootNode.setChildren(geoNodes); - // Rotate -90 degrees around X-axis to convert from Z-up (MDX) to Y-up (glTF) + List rootChildren = new ArrayList<>(); + rootChildren.addAll(geoNodes); + rootChildren.addAll(topLevelBoneNodeIndices); // NEW include bones + if (!rootChildren.isEmpty()) { + rootNode.setChildren(rootChildren); + } rootNode.setRotation(new float[] { -0.7071068f, 0, 0, 0.7071068f }); nodes.add(rootNode); - var rootNodeIndex = nodes.size() - 1; // Get the index of the root node - + int rootNodeIndex = nodes.size() - 1; Scene scene = new Scene(); scene.setNodes(Arrays.asList(rootNodeIndex)); gltf.setScenes(Arrays.asList(scene)); @@ -537,7 +737,9 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, Predicate< gltf.setSamplers(samplers); gltf.setTextures(textures); gltf.setMaterials(materials); - // gltf.setSkins(skins); + if (!skins.isEmpty()) { // NEW set skins only if present + gltf.setSkins(skins); + } } private static class GeosetData { From 04095272c11cfbd91569226b13480093a197145a Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Sat, 9 Aug 2025 11:44:03 +0300 Subject: [PATCH 21/33] add extents --- .../src/com/matrixeater/gltf/GLTFExport.java | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index 1d40913a..8130e248 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -20,6 +20,7 @@ import com.hiveworkshop.wc3.mdl.Bitmap; import com.hiveworkshop.wc3.mdl.Bone; import com.hiveworkshop.wc3.mdl.EditableModel; +import com.hiveworkshop.wc3.mdl.ExtLog; import com.hiveworkshop.wc3.mdl.Geoset; import com.hiveworkshop.wc3.mdl.GeosetAnim; import com.hiveworkshop.wc3.mdl.GeosetVertex; @@ -520,7 +521,13 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, Predicate< var positionBufferViewIndex = bufferViews.size() - 1; Accessor positionAccessor = new Accessor(); - // TODO: should define min/max values for positionAccessor, after I figure out + var minExtents = new Number[]{geoset.getExtents().getMinimumExtent().x, + geoset.getExtents().getMinimumExtent().y, geoset.getExtents().getMinimumExtent().z}; + var maxExtents = new Number[]{geoset.getExtents().getMaximumExtent().x, + geoset.getExtents().getMaximumExtent().y, geoset.getExtents().getMaximumExtent().z}; + + positionAccessor.setMax(maxExtents); // Default max extent + positionAccessor.setMin(minExtents); // Updated to use calculated min extents // how vertices are expresssed in the model positionAccessor.setBufferView(positionBufferViewIndex); positionAccessor.setComponentType(5126); // FLOAT @@ -695,8 +702,7 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, Predicate< } primitive.setIndices(indicesAccessorIndex); primitive.setMode(4); // TRIANGLES - primitive.setMaterial(data.materialIndex); // Assuming materialIndex is the index of the material in the - // glTF + primitive.setMaterial(data.materialIndex); mesh.setPrimitives(Arrays.asList(primitive)); meshes.add(mesh); var meshIndex = meshes.size() - 1; // Get the index of the mesh @@ -720,7 +726,7 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, Predicate< if (!rootChildren.isEmpty()) { rootNode.setChildren(rootChildren); } - rootNode.setRotation(new float[] { -0.7071068f, 0, 0, 0.7071068f }); + rootNode.setRotation(new float[] { -0.7071068f, 0, 0, 0.7071068f }); // lazy rotation to match expected axis nodes.add(rootNode); int rootNodeIndex = nodes.size() - 1; Scene scene = new Scene(); @@ -877,8 +883,6 @@ private static boolean isGeosetTeamGlow(Geoset geoset) { } // Best-effort sampler without wiring a full AnimatedRenderEnvironment: - // - If vis flag exists but we can’t interpolate precisely, fall back to static - // (This keeps the change safe; refine later by wiring a proper time env.) private static float sampleGeosetVisibilityAtTime(GeosetAnim ga, int time) { try { // Prefer the existing helper if available @@ -893,8 +897,6 @@ private static float sampleGeosetVisibilityAtTime(GeosetAnim ga, int time) { // Heuristic: if flag has only one keyframe, use its value; otherwise assume // visible - // NOTE: Replace this with proper interpolation using your AnimFlag API if - // available. if (visFlag.size() == 0) { double staticAlpha = ga.getStaticAlpha(); return (float) (staticAlpha == -1 ? 1.0 : staticAlpha); @@ -910,7 +912,7 @@ private static float sampleGeosetVisibilityAtTime(GeosetAnim ga, int time) { Object v = visFlag.getValues().get(i); if (v instanceof Number) { float vis = ((Number) v).floatValue(); - if (vis < 0.9f) { // Threshold for visibility + if (vis < 0.9f) { // Threshold for visibility, I assume alpha is generally either 0 or 1, so 0.9 is as good as any value return vis; // Return the visibility value } } From a83a330cd8cf2f6c9c9e5dbb89b5f17de31922e5 Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Sat, 9 Aug 2025 11:59:12 +0300 Subject: [PATCH 22/33] apply rotation to top bones to satisfy validator --- .../src/com/matrixeater/gltf/GLTFExport.java | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index 8130e248..840737a0 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -428,11 +428,13 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, Predicate< List topLevelBoneNodeIndices = new ArrayList<>(); for (Bone bone : mdxBones) { if (!(bone.getParent() instanceof Bone)) { - topLevelBoneNodeIndices.add(boneToNode.get(bone)); + var topLevelBoneIndex = boneToNode.get(bone); + topLevelBoneNodeIndices.add(topLevelBoneIndex); + Node topLevelBoneNode = nodes.get(topLevelBoneIndex); + topLevelBoneNode.setRotation(new float[] { -0.7071068f, 0, 0, 0.7071068f }); // lazy rotation to match expected axis } } - // NEW: Skin (one skin covering all bones) int skinIndex = -1; if (!mdxBones.isEmpty()) { int boneCount = mdxBones.size(); @@ -546,9 +548,6 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, Predicate< indicesBuffer.setByteLength(indicesBytes.length); indicesBuffer.setUri(indicesUri); buffers.add(indicesBuffer); - // gltf.getBuffers().add(indicesBuffer); // Updated to use getBuffers(), now - // it's not null because we added - // positionBuffer var indicesBufferIndex = buffers.size() - 1; // Get the index of the indices buffer BufferView indicesBufferView = new BufferView(); @@ -717,20 +716,12 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, Predicate< geoNodes.add(nodes.size() - 1); } - // Merge root - Node rootNode = new Node(); - rootNode.setName(model.getName()); List rootChildren = new ArrayList<>(); rootChildren.addAll(geoNodes); - rootChildren.addAll(topLevelBoneNodeIndices); // NEW include bones - if (!rootChildren.isEmpty()) { - rootNode.setChildren(rootChildren); - } - rootNode.setRotation(new float[] { -0.7071068f, 0, 0, 0.7071068f }); // lazy rotation to match expected axis - nodes.add(rootNode); - int rootNodeIndex = nodes.size() - 1; + rootChildren.addAll(topLevelBoneNodeIndices); + Scene scene = new Scene(); - scene.setNodes(Arrays.asList(rootNodeIndex)); + scene.setNodes(rootChildren); gltf.setScenes(Arrays.asList(scene)); gltf.setScene(0); From fe0b69fb6cd05c153af82c55913286d8dac11f37 Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Sat, 9 Aug 2025 13:30:25 +0300 Subject: [PATCH 23/33] Simplify code --- .../src/com/matrixeater/gltf/GLTFExport.java | 59 +++++++++++-------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index 840737a0..3a6fbeb4 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -13,9 +13,19 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.function.BiFunction; import java.util.function.Predicate; import java.util.logging.Logger; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.SwingUtilities; +import javax.swing.JButton; +import javax.swing.JCheckBox; + import com.hiveworkshop.wc3.gui.datachooser.DataSource; import com.hiveworkshop.wc3.mdl.Bitmap; import com.hiveworkshop.wc3.mdl.Bone; @@ -41,7 +51,6 @@ import de.javagl.jgltf.impl.v2.MeshPrimitive; import de.javagl.jgltf.impl.v2.Node; import de.javagl.jgltf.impl.v2.Scene; -import de.javagl.jgltf.model.animation.AnimationManager.AnimationPolicy; import de.javagl.jgltf.model.io.GltfWriter; import de.javagl.jgltf.impl.v2.*; import de.wc3data.stream.BlizzardDataInputStream; @@ -62,10 +71,10 @@ public GLTFExport(MainPanel mainframe) { @Override public void actionPerformed(ActionEvent e) { // Build a simple modal dialog for export options - final javax.swing.JDialog dialog = new javax.swing.JDialog( - (java.awt.Frame) javax.swing.SwingUtilities.getWindowAncestor(mainframe), + final JDialog dialog = new JDialog( + (java.awt.Frame) SwingUtilities.getWindowAncestor(mainframe), "GLTF Export", true); - javax.swing.JPanel panel = new javax.swing.JPanel(new java.awt.GridBagLayout()); + JPanel panel = new JPanel(new java.awt.GridBagLayout()); java.awt.GridBagConstraints gc = new java.awt.GridBagConstraints(); gc.insets = new java.awt.Insets(4, 4, 4, 4); gc.anchor = java.awt.GridBagConstraints.WEST; @@ -73,14 +82,14 @@ public void actionPerformed(ActionEvent e) { gc.gridy = 0; // Checkbox for visibility-by-animation filtering - final javax.swing.JCheckBox visibilityCheck = new javax.swing.JCheckBox("Filter by animation visibility"); + final JCheckBox visibilityCheck = new JCheckBox("Filter by animation visibility"); panel.add(visibilityCheck, gc); // Animation text field (name or index) gc.gridy++; - panel.add(new javax.swing.JLabel("Animation (name or index):"), gc); + panel.add(new JLabel("Animation (name or index):"), gc); gc.gridx = 1; - final javax.swing.JTextField animationField = new javax.swing.JTextField(18); + final JTextField animationField = new JTextField(18); animationField.setEnabled(false); panel.add(animationField, gc); @@ -89,21 +98,21 @@ public void actionPerformed(ActionEvent e) { // Buttons gc.gridx = 0; gc.gridy++; - final javax.swing.JButton exportCurrentBtn = new javax.swing.JButton("Export Current Model"); + final JButton exportCurrentBtn = new JButton("Export Current Model"); panel.add(exportCurrentBtn, gc); gc.gridx = 1; - final javax.swing.JButton exportAllBtn = new javax.swing.JButton("Export All Models"); + final JButton exportAllBtn = new JButton("Export All Models"); panel.add(exportAllBtn, gc); gc.gridx = 0; gc.gridy++; - final javax.swing.JButton closeBtn = new javax.swing.JButton("Close"); + final JButton closeBtn = new JButton("Close"); panel.add(closeBtn, gc); closeBtn.addActionListener(ev -> dialog.dispose()); // Helper to resolve animation by text (index or partial name) - java.util.function.BiFunction resolveAnimation = ( + BiFunction resolveAnimation = ( model, text) -> { if (model == null || text == null || text.isBlank()) return null; @@ -143,8 +152,8 @@ public void actionPerformed(ActionEvent e) { var model = mainframe.currentMDL(); if (model == null) { log.warning("No current model to export."); - javax.swing.SwingUtilities.invokeLater(() -> javax.swing.JOptionPane.showMessageDialog(dialog, - "No current model loaded.", "Export", javax.swing.JOptionPane.WARNING_MESSAGE)); + SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(dialog, + "No current model loaded.", "Export", JOptionPane.WARNING_MESSAGE)); return; } com.hiveworkshop.wc3.mdl.Animation anim = null; @@ -159,17 +168,17 @@ public void actionPerformed(ActionEvent e) { var predicate = predicateFor.apply(model, anim); GLTFExport.export(model, predicate); log.info("Exported current model: " + model.getName()); - javax.swing.SwingUtilities.invokeLater(() -> javax.swing.JOptionPane.showMessageDialog(dialog, - "Exported: " + model.getName(), "Export", javax.swing.JOptionPane.INFORMATION_MESSAGE)); + SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(dialog, + "Exported: " + model.getName(), "Export", JOptionPane.INFORMATION_MESSAGE)); } catch (Exception ex2) { log.severe("Export failed: " + ex2.getMessage()); - javax.swing.SwingUtilities.invokeLater(() -> javax.swing.JOptionPane.showMessageDialog(dialog, + SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(dialog, "Export failed: " + ex2.getMessage(), "Export Error", - javax.swing.JOptionPane.ERROR_MESSAGE)); + JOptionPane.ERROR_MESSAGE)); // print stack trace for debugging ex2.printStackTrace(); } finally { - javax.swing.SwingUtilities.invokeLater(() -> exportCurrentBtn.setEnabled(true)); + SwingUtilities.invokeLater(() -> exportCurrentBtn.setEnabled(true)); } }, "GLTF-Export-Current").start(); }); @@ -206,17 +215,22 @@ public void actionPerformed(ActionEvent e) { } int finalSuccess = success; int finalFail = fail; - javax.swing.SwingUtilities.invokeLater(() -> javax.swing.JOptionPane.showMessageDialog(dialog, + SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(dialog, "All exports complete.\nSuccess: " + finalSuccess + "\nFailed: " + finalFail, - "Export All", javax.swing.JOptionPane.INFORMATION_MESSAGE)); + "Export All", JOptionPane.INFORMATION_MESSAGE)); } finally { - javax.swing.SwingUtilities.invokeLater(() -> exportAllBtn.setEnabled(true)); + SwingUtilities.invokeLater(() -> exportAllBtn.setEnabled(true)); } }, "GLTF-Export-All").start(); }); dialog.getContentPane().add(panel); dialog.pack(); + JOptionPane.showMessageDialog( + dialog, + "GLTF export is experimental.\nResults may be incomplete or incorrect.", + "Experimental Feature", + JOptionPane.WARNING_MESSAGE); dialog.setLocationRelativeTo(mainframe); dialog.setVisible(true); } @@ -357,7 +371,7 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, Predicate< glMaterial.setName(material.getName()); // glMaterial.setAlphaMode("BLEND"); // glMaterial.setAlphaCutoff(null); - // ! The best approximation I could find so far + // ! The best approximation I could find so far, works well on the models I tested glMaterial.setAlphaMode("MASK"); glMaterial.setAlphaCutoff(0.5f); // or whatever cutoff works best @@ -595,7 +609,6 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, Predicate< accessors.add(uvAccessor); var uvAccessorIndex = accessors.size() - 1; - // NEW: Skinning attributes Integer jointsAccessorIndex = null; Integer weightsAccessorIndex = null; if (skinIndex >= 0) { From cd08fdb0be24447ba33ec552be2dd016d1830549 Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Sat, 9 Aug 2025 14:01:08 +0300 Subject: [PATCH 24/33] remove BiFunction and add warning --- .../src/com/matrixeater/gltf/GLTFExport.java | 65 ++++++++++--------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index 3a6fbeb4..2f2ecb42 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -14,7 +14,6 @@ import java.util.List; import java.util.Map; import java.util.function.BiFunction; -import java.util.function.Predicate; import java.util.logging.Logger; import javax.swing.JDialog; @@ -135,15 +134,6 @@ public void actionPerformed(ActionEvent e) { return null; }; - // Predicate factory - java.util.function.BiFunction> predicateFor = ( - model, anim) -> { - if (anim == null) { - return g -> true; - } - return g -> isGeosetVisibleInAnimation(g, anim); - }; - // Export current model action exportCurrentBtn.addActionListener(ev -> { exportCurrentBtn.setEnabled(false); @@ -165,8 +155,7 @@ public void actionPerformed(ActionEvent e) { log.info("Using animation for visibility filter: " + anim.getName()); } } - var predicate = predicateFor.apply(model, anim); - GLTFExport.export(model, predicate); + GLTFExport.export(model, anim, "models"); log.info("Exported current model: " + model.getName()); SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(dialog, "Exported: " + model.getName(), "Export", JOptionPane.INFORMATION_MESSAGE)); @@ -188,31 +177,47 @@ public void actionPerformed(ActionEvent e) { exportAllBtn.setEnabled(false); new Thread(() -> { try { - java.util.List paths = new java.util.ArrayList<>(); - paths.addAll(getAllUnitPaths()); - paths.addAll(getAllDoodadsPaths()); - log.info("Beginning export of " + paths.size() + " models."); int success = 0; int fail = 0; - for (String path : paths) { + + // Units + for (String path : getAllUnitPaths()) { try { var model = loadModel(path); if (model == null) { fail++; continue; } - com.hiveworkshop.wc3.mdl.Animation anim = null; - if (visibilityCheck.isSelected()) { - anim = resolveAnimation.apply(model, animationField.getText()); + com.hiveworkshop.wc3.mdl.Animation anim = visibilityCheck.isSelected() + ? resolveAnimation.apply(model, animationField.getText()) + : null; + GLTFExport.export(model, anim, "models/units"); + success++; + } catch (Exception one) { + fail++; + log.warning("Failed exporting unit " + path + ": " + one.getMessage()); + } + } + + // Doodads + for (String path : getAllDoodadsPaths()) { + try { + var model = loadModel(path); + if (model == null) { + fail++; + continue; } - var predicate = predicateFor.apply(model, anim); - GLTFExport.export(model, predicate); + com.hiveworkshop.wc3.mdl.Animation anim = visibilityCheck.isSelected() + ? resolveAnimation.apply(model, animationField.getText()) + : null; + GLTFExport.export(model, anim, "models/doodads"); success++; } catch (Exception one) { fail++; - log.warning("Failed exporting " + path + ": " + one.getMessage()); + log.warning("Failed exporting doodad " + path + ": " + one.getMessage()); } } + int finalSuccess = success; int finalFail = fail; SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(dialog, @@ -273,9 +278,9 @@ private List getAllDoodadsPaths() { return doodadPaths; } - private static void export(EditableModel model, Predicate visibilityFilter) throws IOException { - var gltf = createGltfModel(model, visibilityFilter); - File outputFile = new File("models/" + model.getName() + ".gltf"); + private static void export(EditableModel model, com.hiveworkshop.wc3.mdl.Animation animation, String baseDir) throws IOException { + var gltf = createGltfModel(model, animation); + File outputFile = new File(baseDir + "/" + model.getName() + ".gltf"); // Ensure parent directories exist File parentDir = outputFile.getParentFile(); @@ -294,7 +299,7 @@ private static void export(EditableModel model, Predicate visibilityFilt } } - private static GlTF createGltfModel(EditableModel model, Predicate visibilityFilter) { + private static GlTF createGltfModel(EditableModel model, com.hiveworkshop.wc3.mdl.Animation animation) { GlTF gltf = new GlTF(); Asset asset = new Asset(); @@ -302,12 +307,12 @@ private static GlTF createGltfModel(EditableModel model, Predicate visib asset.setGenerator(model.getName()); gltf.setAsset(asset); - loadMeshIntoModel(model, gltf, visibilityFilter); + loadMeshIntoModel(model, gltf, animation); return gltf; } - private static void loadMeshIntoModel(EditableModel model, GlTF gltf, Predicate visibilityFilter) { + private static void loadMeshIntoModel(EditableModel model, GlTF gltf, com.hiveworkshop.wc3.mdl.Animation animation) { List buffers = new ArrayList<>(); List bufferViews = new ArrayList<>(); @@ -512,7 +517,7 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, Predicate< // log.info("Geosets: " + model.getGeosets().size()); for (Geoset geoset : model.getGeosets()) { - if (!visibilityFilter.test(geoset)) { + if (!isGeosetVisibleInAnimation(geoset, animation)) { log.info("Skipping geoset " + geoset.getName() + " due to visibility filter."); continue; // Skip geosets that are not visible in the animation } From 9685b509f186b13d7dbc78744976d286cbf8abbb Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Sat, 9 Aug 2025 14:51:32 +0300 Subject: [PATCH 25/33] Fix testing visibility against invalid keyframe --- .../src/com/matrixeater/gltf/GLTFExport.java | 116 +++++++++++++----- 1 file changed, 86 insertions(+), 30 deletions(-) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index 2f2ecb42..8a6da23c 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -11,6 +11,7 @@ import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.function.BiFunction; @@ -24,12 +25,12 @@ import javax.swing.SwingUtilities; import javax.swing.JButton; import javax.swing.JCheckBox; +import javax.swing.JProgressBar; import com.hiveworkshop.wc3.gui.datachooser.DataSource; import com.hiveworkshop.wc3.mdl.Bitmap; import com.hiveworkshop.wc3.mdl.Bone; import com.hiveworkshop.wc3.mdl.EditableModel; -import com.hiveworkshop.wc3.mdl.ExtLog; import com.hiveworkshop.wc3.mdl.Geoset; import com.hiveworkshop.wc3.mdl.GeosetAnim; import com.hiveworkshop.wc3.mdl.GeosetVertex; @@ -103,6 +104,19 @@ public void actionPerformed(ActionEvent e) { final JButton exportAllBtn = new JButton("Export All Models"); panel.add(exportAllBtn, gc); + // Progress bar row + gc.gridx = 0; + gc.gridy++; + gc.gridwidth = 2; + final JProgressBar exportAllProgress = new JProgressBar(); + exportAllProgress.setStringPainted(true); + exportAllProgress.setMinimum(0); + exportAllProgress.setMaximum(100); + exportAllProgress.setValue(0); + exportAllProgress.setString("Idle"); + panel.add(exportAllProgress, gc); + gc.gridwidth = 1; + gc.gridx = 0; gc.gridy++; final JButton closeBtn = new JButton("Close"); @@ -175,56 +189,101 @@ public void actionPerformed(ActionEvent e) { // Export all models action exportAllBtn.addActionListener(ev -> { exportAllBtn.setEnabled(false); + exportAllProgress.setIndeterminate(true); + exportAllProgress.setString("Preparing..."); new Thread(() -> { try { int success = 0; int fail = 0; + List failedModels = new ArrayList<>(); // track failed exports + // Gather paths first to know total for progress + List unitPaths = getAllUnitPaths(); + List doodadPaths = getAllDoodadsPaths(); + int total = unitPaths.size() + doodadPaths.size(); + SwingUtilities.invokeLater(() -> { + exportAllProgress.setIndeterminate(false); + exportAllProgress.setMaximum(total); + exportAllProgress.setValue(0); + exportAllProgress.setString("0 / " + total); + }); + int processed = 0; // Units - for (String path : getAllUnitPaths()) { + for (String path : unitPaths) { try { var model = loadModel(path); if (model == null) { fail++; - continue; + failedModels.add(path); + } else { + com.hiveworkshop.wc3.mdl.Animation anim = visibilityCheck.isSelected() + ? resolveAnimation.apply(model, animationField.getText()) + : null; + GLTFExport.export(model, anim, "models/units"); + success++; } - com.hiveworkshop.wc3.mdl.Animation anim = visibilityCheck.isSelected() - ? resolveAnimation.apply(model, animationField.getText()) - : null; - GLTFExport.export(model, anim, "models/units"); - success++; } catch (Exception one) { fail++; + failedModels.add(path); log.warning("Failed exporting unit " + path + ": " + one.getMessage()); } + processed++; + final int fProcessed = processed; + final int fTotal = total; + SwingUtilities.invokeLater(() -> { + exportAllProgress.setValue(fProcessed); + exportAllProgress.setString(fProcessed + " / " + fTotal); + }); } // Doodads - for (String path : getAllDoodadsPaths()) { + for (String path : doodadPaths) { try { var model = loadModel(path); if (model == null) { fail++; - continue; + failedModels.add(path); + } else { + com.hiveworkshop.wc3.mdl.Animation anim = visibilityCheck.isSelected() + ? resolveAnimation.apply(model, animationField.getText()) + : null; + GLTFExport.export(model, anim, "models/doodads"); + success++; } - com.hiveworkshop.wc3.mdl.Animation anim = visibilityCheck.isSelected() - ? resolveAnimation.apply(model, animationField.getText()) - : null; - GLTFExport.export(model, anim, "models/doodads"); - success++; } catch (Exception one) { fail++; + failedModels.add(path); log.warning("Failed exporting doodad " + path + ": " + one.getMessage()); } + processed++; + final int fProcessed = processed; + final int fTotal = total; + SwingUtilities.invokeLater(() -> { + exportAllProgress.setValue(fProcessed); + exportAllProgress.setString(fProcessed + " / " + fTotal); + }); } int finalSuccess = success; int finalFail = fail; - SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(dialog, - "All exports complete.\nSuccess: " + finalSuccess + "\nFailed: " + finalFail, - "Export All", JOptionPane.INFORMATION_MESSAGE)); + List finalFailedModels = new ArrayList<>(failedModels); + SwingUtilities.invokeLater(() -> { + exportAllProgress.setString("Done: " + finalSuccess + "/" + total + " (Failed " + finalFail + ")"); + StringBuilder msg = new StringBuilder(); + msg.append("All exports complete.\nSuccess: ").append(finalSuccess).append("\nFailed: ").append(finalFail); + if (finalFail > 0) { + msg.append("\nFailed models (paths):\n"); + for (String fm : finalFailedModels) { + msg.append(fm).append('\n'); + } + } + JOptionPane.showMessageDialog(dialog, msg.toString(), "Export All", JOptionPane.INFORMATION_MESSAGE); + }); } finally { - SwingUtilities.invokeLater(() -> exportAllBtn.setEnabled(true)); + SwingUtilities.invokeLater(() -> { + exportAllBtn.setEnabled(true); + exportAllProgress.setIndeterminate(false); + }); } }, "GLTF-Export-All").start(); }); @@ -916,25 +975,22 @@ private static float sampleGeosetVisibilityAtTime(GeosetAnim ga, int time) { } return 1.0f; } else { - for (int i = 0; i < visFlag.size(); i++) { - if (visFlag.getTimes().get(i) >= time) { - Object v = visFlag.getValues().get(i); - if (v instanceof Number) { - float vis = ((Number) v).floatValue(); - if (vis < 0.9f) { // Threshold for visibility, I assume alpha is generally either 0 or 1, so 0.9 is as good as any value - return vis; // Return the visibility value - } - } - return 1.0f; // Fallback to visible + //times is sorted, hopefully no duplicates, so Collections.binarySearch is behaving like a bisect left + int keyframeTime = Collections.binarySearch(visFlag.getTimes(), time); + Object v = visFlag.getValues().get(keyframeTime); + if (v instanceof Number) { + float vis = ((Number) v).floatValue(); + if (vis < 0.9f) { // Threshold for visibility, I assume alpha is generally either 0 or 1, so 0.9 is as good as any value + return vis; // Return the visibility value } } + return 1.0f; // Fallback to visible } } catch (Throwable t) { // Fallback: do not exclude on errors double staticAlpha = ga.getStaticAlpha(); return (float) (staticAlpha == -1 ? 1.0 : staticAlpha); } - return 1.0f; // Default visible if all else fails } private EditableModel loadModel(String path) { From 9bcb91d5c64ae5b158cef64db6cb76d98e237f30 Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Sat, 9 Aug 2025 15:05:57 +0300 Subject: [PATCH 26/33] Improve export all report with timing data --- .../src/com/matrixeater/gltf/GLTFExport.java | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index 8a6da23c..524d55f0 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -23,9 +23,14 @@ import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.SwingUtilities; +import javax.swing.Box; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JProgressBar; +import javax.swing.JTextArea; +import javax.swing.JScrollPane; +import java.awt.BorderLayout; +import java.awt.Dimension; import com.hiveworkshop.wc3.gui.datachooser.DataSource; import com.hiveworkshop.wc3.mdl.Bitmap; @@ -192,6 +197,7 @@ public void actionPerformed(ActionEvent e) { exportAllProgress.setIndeterminate(true); exportAllProgress.setString("Preparing..."); new Thread(() -> { + long startNanos = System.nanoTime(); // timing start try { int success = 0; int fail = 0; @@ -267,17 +273,28 @@ public void actionPerformed(ActionEvent e) { int finalSuccess = success; int finalFail = fail; List finalFailedModels = new ArrayList<>(failedModels); + long elapsedMs = (System.nanoTime() - startNanos) / 1_000_000L; // timing end SwingUtilities.invokeLater(() -> { + double secs = elapsedMs / 1000.0; exportAllProgress.setString("Done: " + finalSuccess + "/" + total + " (Failed " + finalFail + ")"); - StringBuilder msg = new StringBuilder(); - msg.append("All exports complete.\nSuccess: ").append(finalSuccess).append("\nFailed: ").append(finalFail); if (finalFail > 0) { - msg.append("\nFailed models (paths):\n"); - for (String fm : finalFailedModels) { - msg.append(fm).append('\n'); - } + var msgPanel = Box.createVerticalBox(); + String summary = "Success: " + finalSuccess + "\nFailed: " + finalFail + "\nDuration: " + String.format(java.util.Locale.US, "%.3f s", secs); + msgPanel.add(new JLabel("Export Summary"), BorderLayout.NORTH); + msgPanel.add(new JTextArea(summary), BorderLayout.NORTH); + msgPanel.add(new JLabel("Failed models (paths):"), BorderLayout.NORTH); + JTextArea failList = new JTextArea(); + failList.setEditable(false); + failList.setText(String.join("\n", finalFailedModels)); + JScrollPane scrollPane = new JScrollPane(failList); + scrollPane.setPreferredSize(new Dimension(400, 300)); + msgPanel.add(scrollPane, BorderLayout.CENTER); + JOptionPane.showMessageDialog(dialog, msgPanel, "Export All", JOptionPane.INFORMATION_MESSAGE); + } else { + JOptionPane.showMessageDialog(dialog, + "All exports complete.\nSuccess: " + finalSuccess + "\nFailed: 0\nDuration: " + String.format(java.util.Locale.US, "%.3f s", secs), + "Export All", JOptionPane.INFORMATION_MESSAGE); } - JOptionPane.showMessageDialog(dialog, msg.toString(), "Export All", JOptionPane.INFORMATION_MESSAGE); }); } finally { SwingUtilities.invokeLater(() -> { From 0175f661df148ab37df302f369cd2d93b1f90184 Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Sat, 9 Aug 2025 15:38:48 +0300 Subject: [PATCH 27/33] Thread the export --- .../src/com/matrixeater/gltf/GLTFExport.java | 159 ++++++++++++------ 1 file changed, 103 insertions(+), 56 deletions(-) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index 524d55f0..a6fe63b5 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -14,6 +14,9 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiFunction; import java.util.logging.Logger; @@ -199,9 +202,7 @@ public void actionPerformed(ActionEvent e) { new Thread(() -> { long startNanos = System.nanoTime(); // timing start try { - int success = 0; - int fail = 0; - List failedModels = new ArrayList<>(); // track failed exports + List failedModels = Collections.synchronizedList(new ArrayList<>()); // track failed exports // Gather paths first to know total for progress List unitPaths = getAllUnitPaths(); List doodadPaths = getAllDoodadsPaths(); @@ -212,34 +213,50 @@ public void actionPerformed(ActionEvent e) { exportAllProgress.setValue(0); exportAllProgress.setString("0 / " + total); }); - int processed = 0; + + AtomicInteger success = new AtomicInteger(0); + AtomicInteger fail = new AtomicInteger(0); + AtomicInteger processed = new AtomicInteger(0); + var threads = Math.max(1, Runtime.getRuntime().availableProcessors() - 1); + ExecutorService executor = java.util.concurrent.Executors.newFixedThreadPool(threads); // Units for (String path : unitPaths) { try { var model = loadModel(path); if (model == null) { - fail++; + fail.incrementAndGet(); failedModels.add(path); } else { - com.hiveworkshop.wc3.mdl.Animation anim = visibilityCheck.isSelected() - ? resolveAnimation.apply(model, animationField.getText()) - : null; - GLTFExport.export(model, anim, "models/units"); - success++; + executor.submit(() -> { + com.hiveworkshop.wc3.mdl.Animation anim = visibilityCheck.isSelected() + ? resolveAnimation.apply(model, animationField.getText()) + : null; + try { + GLTFExport.export(model, anim, "models/units"); + success.incrementAndGet(); + } catch (Exception ex) { + log.warning("Failed exporting unit " + path + ": " + ex.getMessage()); + fail.incrementAndGet(); + failedModels.add(path); + return; + } finally { + processed.incrementAndGet(); + final int fProcessed = processed.get(); + final int fTotal = total; + SwingUtilities.invokeLater(() -> { + exportAllProgress.setValue(fProcessed); + exportAllProgress.setString(fProcessed + " / " + fTotal); + }); + } + }); } } catch (Exception one) { - fail++; + fail.incrementAndGet(); failedModels.add(path); log.warning("Failed exporting unit " + path + ": " + one.getMessage()); } - processed++; - final int fProcessed = processed; - final int fTotal = total; - SwingUtilities.invokeLater(() -> { - exportAllProgress.setValue(fProcessed); - exportAllProgress.setString(fProcessed + " / " + fTotal); - }); + } // Doodads @@ -247,39 +264,56 @@ public void actionPerformed(ActionEvent e) { try { var model = loadModel(path); if (model == null) { - fail++; + fail.incrementAndGet(); failedModels.add(path); } else { - com.hiveworkshop.wc3.mdl.Animation anim = visibilityCheck.isSelected() - ? resolveAnimation.apply(model, animationField.getText()) - : null; - GLTFExport.export(model, anim, "models/doodads"); - success++; + executor.submit(() -> { + com.hiveworkshop.wc3.mdl.Animation anim = visibilityCheck.isSelected() + ? resolveAnimation.apply(model, animationField.getText()) + : null; + try { + GLTFExport.export(model, anim, "models/doodads"); + success.incrementAndGet(); + } + catch (Exception ex) { + log.warning("Failed exporting doodad " + path + ": " + ex.getMessage()); + fail.incrementAndGet(); + failedModels.add(path); + return; + } + finally { + processed.incrementAndGet(); + final int fProcessed = processed.get(); + final int fTotal = total; + SwingUtilities.invokeLater(() -> { + exportAllProgress.setValue(fProcessed); + exportAllProgress.setString(fProcessed + " / " + fTotal); + }); + } + }); } } catch (Exception one) { - fail++; + fail.incrementAndGet(); failedModels.add(path); log.warning("Failed exporting doodad " + path + ": " + one.getMessage()); } - processed++; - final int fProcessed = processed; - final int fTotal = total; - SwingUtilities.invokeLater(() -> { - exportAllProgress.setValue(fProcessed); - exportAllProgress.setString(fProcessed + " / " + fTotal); - }); + } + executor.shutdown(); + executor.awaitTermination(1_000_000, TimeUnit.SECONDS); - int finalSuccess = success; - int finalFail = fail; + int finalSuccess = success.get(); + int finalFail = fail.get(); List finalFailedModels = new ArrayList<>(failedModels); long elapsedMs = (System.nanoTime() - startNanos) / 1_000_000L; // timing end SwingUtilities.invokeLater(() -> { double secs = elapsedMs / 1000.0; - exportAllProgress.setString("Done: " + finalSuccess + "/" + total + " (Failed " + finalFail + ")"); + exportAllProgress + .setString("Done: " + finalSuccess + "/" + total + " (Failed " + finalFail + ")"); if (finalFail > 0) { var msgPanel = Box.createVerticalBox(); - String summary = "Success: " + finalSuccess + "\nFailed: " + finalFail + "\nDuration: " + String.format(java.util.Locale.US, "%.3f s", secs); + String summary = "Success: " + finalSuccess + "\nFailed: " + finalFail + "\nDuration: " + + String.format(java.util.Locale.US, "%.3f s", secs); msgPanel.add(new JLabel("Export Summary"), BorderLayout.NORTH); msgPanel.add(new JTextArea(summary), BorderLayout.NORTH); msgPanel.add(new JLabel("Failed models (paths):"), BorderLayout.NORTH); @@ -289,13 +323,20 @@ public void actionPerformed(ActionEvent e) { JScrollPane scrollPane = new JScrollPane(failList); scrollPane.setPreferredSize(new Dimension(400, 300)); msgPanel.add(scrollPane, BorderLayout.CENTER); - JOptionPane.showMessageDialog(dialog, msgPanel, "Export All", JOptionPane.INFORMATION_MESSAGE); + JOptionPane.showMessageDialog(dialog, msgPanel, "Export All", + JOptionPane.INFORMATION_MESSAGE); } else { JOptionPane.showMessageDialog(dialog, - "All exports complete.\nSuccess: " + finalSuccess + "\nFailed: 0\nDuration: " + String.format(java.util.Locale.US, "%.3f s", secs), + "All exports complete.\nSuccess: " + finalSuccess + "\nFailed: 0\nDuration: " + + String.format(java.util.Locale.US, "%.3f s", secs), "Export All", JOptionPane.INFORMATION_MESSAGE); } }); + } catch (InterruptedException e2) { + log.warning("Export interrupted: " + e2.getMessage()); + SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(dialog, + "Export interrupted: " + e2.getMessage(), "Export Error", + JOptionPane.ERROR_MESSAGE)); } finally { SwingUtilities.invokeLater(() -> { exportAllBtn.setEnabled(true); @@ -354,7 +395,8 @@ private List getAllDoodadsPaths() { return doodadPaths; } - private static void export(EditableModel model, com.hiveworkshop.wc3.mdl.Animation animation, String baseDir) throws IOException { + private static void export(EditableModel model, com.hiveworkshop.wc3.mdl.Animation animation, String baseDir) + throws IOException { var gltf = createGltfModel(model, animation); File outputFile = new File(baseDir + "/" + model.getName() + ".gltf"); @@ -388,7 +430,8 @@ private static GlTF createGltfModel(EditableModel model, com.hiveworkshop.wc3.md return gltf; } - private static void loadMeshIntoModel(EditableModel model, GlTF gltf, com.hiveworkshop.wc3.mdl.Animation animation) { + private static void loadMeshIntoModel(EditableModel model, GlTF gltf, + com.hiveworkshop.wc3.mdl.Animation animation) { List buffers = new ArrayList<>(); List bufferViews = new ArrayList<>(); @@ -452,7 +495,8 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, com.hivewo glMaterial.setName(material.getName()); // glMaterial.setAlphaMode("BLEND"); // glMaterial.setAlphaCutoff(null); - // ! The best approximation I could find so far, works well on the models I tested + // ! The best approximation I could find so far, works well on the models I + // tested glMaterial.setAlphaMode("MASK"); glMaterial.setAlphaCutoff(0.5f); // or whatever cutoff works best @@ -493,8 +537,7 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, com.hivewo kids = new ArrayList<>(); kids.add(boneToNode.get(bone)); pNode.setChildren(kids); - } - else { + } else { kids.add(boneToNode.get(bone)); } } @@ -526,7 +569,8 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, com.hivewo var topLevelBoneIndex = boneToNode.get(bone); topLevelBoneNodeIndices.add(topLevelBoneIndex); Node topLevelBoneNode = nodes.get(topLevelBoneIndex); - topLevelBoneNode.setRotation(new float[] { -0.7071068f, 0, 0, 0.7071068f }); // lazy rotation to match expected axis + topLevelBoneNode.setRotation(new float[] { -0.7071068f, 0, 0, 0.7071068f }); // lazy rotation to match + // expected axis } } @@ -550,9 +594,9 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, com.hivewo ibm[o + 10] = 1; ibm[o + 15] = 1; // translation components (last column except bottom-right) - ibm[o + 12] = (float)(-px); - ibm[o + 13] = (float)(-py); - ibm[o + 14] = (float)(-pz); + ibm[o + 12] = (float) (-px); + ibm[o + 13] = (float) (-py); + ibm[o + 14] = (float) (-pz); } byte[] ibmBytes = new byte[ibm.length * 4]; ByteBuffer.wrap(ibmBytes).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer().put(ibm); @@ -618,10 +662,10 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, com.hivewo var positionBufferViewIndex = bufferViews.size() - 1; Accessor positionAccessor = new Accessor(); - var minExtents = new Number[]{geoset.getExtents().getMinimumExtent().x, - geoset.getExtents().getMinimumExtent().y, geoset.getExtents().getMinimumExtent().z}; - var maxExtents = new Number[]{geoset.getExtents().getMaximumExtent().x, - geoset.getExtents().getMaximumExtent().y, geoset.getExtents().getMaximumExtent().z}; + var minExtents = new Number[] { geoset.getExtents().getMinimumExtent().x, + geoset.getExtents().getMinimumExtent().y, geoset.getExtents().getMinimumExtent().z }; + var maxExtents = new Number[] { geoset.getExtents().getMaximumExtent().x, + geoset.getExtents().getMaximumExtent().y, geoset.getExtents().getMaximumExtent().z }; positionAccessor.setMax(maxExtents); // Default max extent positionAccessor.setMin(minExtents); // Updated to use calculated min extents @@ -704,7 +748,8 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, com.hivewo for (int i = 0; i < influenceCount; i++) { GeosetVertexBoneLink link = links.get(i); Integer nodeIdx = boneToNode.get(link.bone); - if (nodeIdx == null) continue; + if (nodeIdx == null) + continue; joints[v * 4 + i] = (short) (int) nodeIdx; weights[v * 4 + i] = link.weight; total += link.weight; @@ -947,7 +992,7 @@ private static boolean isGeosetVisibleInAnimation(Geoset geoset, com.hiveworksho } private static boolean isGeosetTeamGlow(Geoset geoset) { - //ge the material + // ge the material if (geoset.getMaterial() == null) { return false; // cannot be team glow, team glow has a material } @@ -992,12 +1037,14 @@ private static float sampleGeosetVisibilityAtTime(GeosetAnim ga, int time) { } return 1.0f; } else { - //times is sorted, hopefully no duplicates, so Collections.binarySearch is behaving like a bisect left + // times is sorted, hopefully no duplicates, so Collections.binarySearch is + // behaving like a bisect left int keyframeTime = Collections.binarySearch(visFlag.getTimes(), time); Object v = visFlag.getValues().get(keyframeTime); if (v instanceof Number) { float vis = ((Number) v).floatValue(); - if (vis < 0.9f) { // Threshold for visibility, I assume alpha is generally either 0 or 1, so 0.9 is as good as any value + if (vis < 0.9f) { // Threshold for visibility, I assume alpha is generally either 0 or 1, so 0.9 + // is as good as any value return vis; // Return the visibility value } } @@ -1010,7 +1057,7 @@ private static float sampleGeosetVisibilityAtTime(GeosetAnim ga, int time) { } } - private EditableModel loadModel(String path) { + private static EditableModel loadModel(String path) { var f = MpqCodebase.get().getResourceAsStream(path); try (BlizzardDataInputStream in = new BlizzardDataInputStream(f)) { final EditableModel model = new EditableModel(MdxUtils.loadModel(in)); From 2db30692da779d3c1dae2f255795cf7f222cd091 Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Sat, 9 Aug 2025 15:47:07 +0300 Subject: [PATCH 28/33] Remove rdundant geoset info --- matrixeater/src/com/matrixeater/gltf/GLTFExport.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index a6fe63b5..8365d65c 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -219,7 +219,6 @@ public void actionPerformed(ActionEvent e) { AtomicInteger processed = new AtomicInteger(0); var threads = Math.max(1, Runtime.getRuntime().availableProcessors() - 1); ExecutorService executor = java.util.concurrent.Executors.newFixedThreadPool(threads); - // Units for (String path : unitPaths) { try { @@ -510,7 +509,6 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, materials.add(glMaterial); } // MESH - log.info("Geosets: " + model.getGeosets().size()); List mdxBones = new ArrayList<>(); for (final IdObject object : model.getIdObjects()) { if (object instanceof Bone) { From c8d803af50958923f118b50e124c0a59aba67349 Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Sat, 9 Aug 2025 15:54:58 +0300 Subject: [PATCH 29/33] Remove redundant log --- matrixeater/src/com/matrixeater/gltf/GLTFExport.java | 1 + 1 file changed, 1 insertion(+) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index 8365d65c..d114165a 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -509,6 +509,7 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, materials.add(glMaterial); } // MESH + log.info("Geosets: " + model.getGeosets().size()); List mdxBones = new ArrayList<>(); for (final IdObject object : model.getIdObjects()) { if (object instanceof Bone) { From a905bf38aec02cc29ad1408cb8dcd0e9a0f5fb4b Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Sat, 9 Aug 2025 17:30:30 +0300 Subject: [PATCH 30/33] Add skeleton to a root node with transform instead of applying transform to every node --- .../src/com/matrixeater/gltf/GLTFExport.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index d114165a..dcd80541 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -566,10 +566,7 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, for (Bone bone : mdxBones) { if (!(bone.getParent() instanceof Bone)) { var topLevelBoneIndex = boneToNode.get(bone); - topLevelBoneNodeIndices.add(topLevelBoneIndex); - Node topLevelBoneNode = nodes.get(topLevelBoneIndex); - topLevelBoneNode.setRotation(new float[] { -0.7071068f, 0, 0, 0.7071068f }); // lazy rotation to match - // expected axis + topLevelBoneNodeIndices.add(topLevelBoneIndex); } } @@ -856,7 +853,15 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, List rootChildren = new ArrayList<>(); rootChildren.addAll(geoNodes); - rootChildren.addAll(topLevelBoneNodeIndices); + + Node rootNode = new Node(); + rootNode.setName("Root"); + rootNode.setChildren(topLevelBoneNodeIndices); // only bones because glTF validator complains if we add skinned meshes as children to a node + rootNode.setRotation(new float[] { -0.7071068f, 0, 0, 0.7071068f }); // lazy rotation to match + // expected axis + nodes.add(rootNode); + var rootNodeIndex = nodes.size() - 1; + rootChildren.add(rootNodeIndex); // Add root node to the scene Scene scene = new Scene(); scene.setNodes(rootChildren); From c343f9ef8c2ebbdde50fd4dabeb333221886f833 Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Sun, 10 Aug 2025 10:53:17 +0300 Subject: [PATCH 31/33] Not thread because loadPngFromMaterial not thread-safe --- .../src/com/matrixeater/gltf/GLTFExport.java | 242 ++++++++---------- 1 file changed, 106 insertions(+), 136 deletions(-) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index dcd80541..8d1fa9bc 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -201,147 +201,118 @@ public void actionPerformed(ActionEvent e) { exportAllProgress.setString("Preparing..."); new Thread(() -> { long startNanos = System.nanoTime(); // timing start - try { - List failedModels = Collections.synchronizedList(new ArrayList<>()); // track failed exports - // Gather paths first to know total for progress - List unitPaths = getAllUnitPaths(); - List doodadPaths = getAllDoodadsPaths(); - int total = unitPaths.size() + doodadPaths.size(); - SwingUtilities.invokeLater(() -> { - exportAllProgress.setIndeterminate(false); - exportAllProgress.setMaximum(total); - exportAllProgress.setValue(0); - exportAllProgress.setString("0 / " + total); - }); - - AtomicInteger success = new AtomicInteger(0); - AtomicInteger fail = new AtomicInteger(0); - AtomicInteger processed = new AtomicInteger(0); - var threads = Math.max(1, Runtime.getRuntime().availableProcessors() - 1); - ExecutorService executor = java.util.concurrent.Executors.newFixedThreadPool(threads); - // Units - for (String path : unitPaths) { + List failedModels = Collections.synchronizedList(new ArrayList<>()); // track failed exports + // Gather paths first to know total for progress + List unitPaths = getAllUnitPaths(); + List doodadPaths = getAllDoodadsPaths(); + int total = unitPaths.size() + doodadPaths.size(); + SwingUtilities.invokeLater(() -> { + exportAllProgress.setIndeterminate(false); + exportAllProgress.setMaximum(total); + exportAllProgress.setValue(0); + exportAllProgress.setString("0 / " + total); + }); + + int success = 0; + int fail = 0; + int processed = 0; + // Units + for (String path : unitPaths) { + var model = loadModel(path); + if (model == null) { + fail++; + failedModels.add(path); + } else { + com.hiveworkshop.wc3.mdl.Animation anim = visibilityCheck.isSelected() + ? resolveAnimation.apply(model, animationField.getText()) + : null; try { - var model = loadModel(path); - if (model == null) { - fail.incrementAndGet(); - failedModels.add(path); - } else { - executor.submit(() -> { - com.hiveworkshop.wc3.mdl.Animation anim = visibilityCheck.isSelected() - ? resolveAnimation.apply(model, animationField.getText()) - : null; - try { - GLTFExport.export(model, anim, "models/units"); - success.incrementAndGet(); - } catch (Exception ex) { - log.warning("Failed exporting unit " + path + ": " + ex.getMessage()); - fail.incrementAndGet(); - failedModels.add(path); - return; - } finally { - processed.incrementAndGet(); - final int fProcessed = processed.get(); - final int fTotal = total; - SwingUtilities.invokeLater(() -> { - exportAllProgress.setValue(fProcessed); - exportAllProgress.setString(fProcessed + " / " + fTotal); - }); - } - }); - } - } catch (Exception one) { - fail.incrementAndGet(); + GLTFExport.export(model, anim, "models/units"); + success++; + } catch (Exception ex) { + log.warning("Failed exporting unit " + path + ": " + ex.getMessage()); + ex.printStackTrace(); + fail++; failedModels.add(path); - log.warning("Failed exporting unit " + path + ": " + one.getMessage()); + } finally { + processed++; + final int fProcessed = processed; + final int fTotal = total; + SwingUtilities.invokeLater(() -> { + exportAllProgress.setValue(fProcessed); + exportAllProgress.setString(fProcessed + " / " + fTotal); + }); } - } + } - // Doodads - for (String path : doodadPaths) { - try { - var model = loadModel(path); - if (model == null) { - fail.incrementAndGet(); + // Doodads + for (String path : doodadPaths) { + try { + var model = loadModel(path); + if (model == null) { + fail++; + failedModels.add(path); + } else { + com.hiveworkshop.wc3.mdl.Animation anim = visibilityCheck.isSelected() + ? resolveAnimation.apply(model, animationField.getText()) + : null; + try { + GLTFExport.export(model, anim, "models/doodads"); + success++; + } catch (Exception ex) { + log.warning("Failed exporting doodad " + path + ": " + ex.getMessage()); + fail++; failedModels.add(path); - } else { - executor.submit(() -> { - com.hiveworkshop.wc3.mdl.Animation anim = visibilityCheck.isSelected() - ? resolveAnimation.apply(model, animationField.getText()) - : null; - try { - GLTFExport.export(model, anim, "models/doodads"); - success.incrementAndGet(); - } - catch (Exception ex) { - log.warning("Failed exporting doodad " + path + ": " + ex.getMessage()); - fail.incrementAndGet(); - failedModels.add(path); - return; - } - finally { - processed.incrementAndGet(); - final int fProcessed = processed.get(); - final int fTotal = total; - SwingUtilities.invokeLater(() -> { - exportAllProgress.setValue(fProcessed); - exportAllProgress.setString(fProcessed + " / " + fTotal); - }); - } + } finally { + processed++; + final int fProcessed = processed; + final int fTotal = total; + SwingUtilities.invokeLater(() -> { + exportAllProgress.setValue(fProcessed); + exportAllProgress.setString(fProcessed + " / " + fTotal); }); } - } catch (Exception one) { - fail.incrementAndGet(); - failedModels.add(path); - log.warning("Failed exporting doodad " + path + ": " + one.getMessage()); } - + } catch (Exception one) { + fail++; + failedModels.add(path); + log.warning("Failed exporting doodad " + path + ": " + one.getMessage()); } - executor.shutdown(); - executor.awaitTermination(1_000_000, TimeUnit.SECONDS); - - int finalSuccess = success.get(); - int finalFail = fail.get(); - List finalFailedModels = new ArrayList<>(failedModels); - long elapsedMs = (System.nanoTime() - startNanos) / 1_000_000L; // timing end - SwingUtilities.invokeLater(() -> { - double secs = elapsedMs / 1000.0; - exportAllProgress - .setString("Done: " + finalSuccess + "/" + total + " (Failed " + finalFail + ")"); - if (finalFail > 0) { - var msgPanel = Box.createVerticalBox(); - String summary = "Success: " + finalSuccess + "\nFailed: " + finalFail + "\nDuration: " - + String.format(java.util.Locale.US, "%.3f s", secs); - msgPanel.add(new JLabel("Export Summary"), BorderLayout.NORTH); - msgPanel.add(new JTextArea(summary), BorderLayout.NORTH); - msgPanel.add(new JLabel("Failed models (paths):"), BorderLayout.NORTH); - JTextArea failList = new JTextArea(); - failList.setEditable(false); - failList.setText(String.join("\n", finalFailedModels)); - JScrollPane scrollPane = new JScrollPane(failList); - scrollPane.setPreferredSize(new Dimension(400, 300)); - msgPanel.add(scrollPane, BorderLayout.CENTER); - JOptionPane.showMessageDialog(dialog, msgPanel, "Export All", - JOptionPane.INFORMATION_MESSAGE); - } else { - JOptionPane.showMessageDialog(dialog, - "All exports complete.\nSuccess: " + finalSuccess + "\nFailed: 0\nDuration: " - + String.format(java.util.Locale.US, "%.3f s", secs), - "Export All", JOptionPane.INFORMATION_MESSAGE); - } - }); - } catch (InterruptedException e2) { - log.warning("Export interrupted: " + e2.getMessage()); - SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(dialog, - "Export interrupted: " + e2.getMessage(), "Export Error", - JOptionPane.ERROR_MESSAGE)); - } finally { - SwingUtilities.invokeLater(() -> { - exportAllBtn.setEnabled(true); - exportAllProgress.setIndeterminate(false); - }); + } + + int finalSuccess = success; + int finalFail = fail; + List finalFailedModels = new ArrayList<>(failedModels); + long elapsedMs = (System.nanoTime() - startNanos) / 1_000_000L; // timing end + SwingUtilities.invokeLater(() -> { + double secs = elapsedMs / 1000.0; + exportAllProgress + .setString("Done: " + finalSuccess + "/" + total + " (Failed " + finalFail + ")"); + if (finalFail > 0) { + var msgPanel = Box.createVerticalBox(); + String summary = "Success: " + finalSuccess + "\nFailed: " + finalFail + "\nDuration: " + + String.format(java.util.Locale.US, "%.3f s", secs); + msgPanel.add(new JLabel("Export Summary"), BorderLayout.NORTH); + msgPanel.add(new JTextArea(summary), BorderLayout.NORTH); + msgPanel.add(new JLabel("Failed models (paths):"), BorderLayout.NORTH); + JTextArea failList = new JTextArea(); + failList.setEditable(false); + failList.setText(String.join("\n", finalFailedModels)); + JScrollPane scrollPane = new JScrollPane(failList); + scrollPane.setPreferredSize(new Dimension(400, 300)); + msgPanel.add(scrollPane, BorderLayout.CENTER); + JOptionPane.showMessageDialog(dialog, msgPanel, "Export All", + JOptionPane.INFORMATION_MESSAGE); + } else { + JOptionPane.showMessageDialog(dialog, + "All exports complete.\nSuccess: " + finalSuccess + "\nFailed: 0\nDuration: " + + String.format(java.util.Locale.US, "%.3f s", secs), + "Export All", JOptionPane.INFORMATION_MESSAGE); + } + }); + }, "GLTF-Export-All").start(); }); @@ -566,7 +537,7 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, for (Bone bone : mdxBones) { if (!(bone.getParent() instanceof Bone)) { var topLevelBoneIndex = boneToNode.get(bone); - topLevelBoneNodeIndices.add(topLevelBoneIndex); + topLevelBoneNodeIndices.add(topLevelBoneIndex); } } @@ -663,8 +634,6 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, var maxExtents = new Number[] { geoset.getExtents().getMaximumExtent().x, geoset.getExtents().getMaximumExtent().y, geoset.getExtents().getMaximumExtent().z }; - positionAccessor.setMax(maxExtents); // Default max extent - positionAccessor.setMin(minExtents); // Updated to use calculated min extents // how vertices are expresssed in the model positionAccessor.setBufferView(positionBufferViewIndex); positionAccessor.setComponentType(5126); // FLOAT @@ -856,10 +825,11 @@ private static void loadMeshIntoModel(EditableModel model, GlTF gltf, Node rootNode = new Node(); rootNode.setName("Root"); - rootNode.setChildren(topLevelBoneNodeIndices); // only bones because glTF validator complains if we add skinned meshes as children to a node + rootNode.setChildren(topLevelBoneNodeIndices); // only bones because glTF validator complains if we add skinned + // meshes as children to a node rootNode.setRotation(new float[] { -0.7071068f, 0, 0, 0.7071068f }); // lazy rotation to match - // expected axis - nodes.add(rootNode); + // expected axis + nodes.add(rootNode); var rootNodeIndex = nodes.size() - 1; rootChildren.add(rootNodeIndex); // Add root node to the scene From 4101561d5eb5a0f0c43fbe0808ee59097f4f970d Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Mon, 8 Dec 2025 23:22:46 +0200 Subject: [PATCH 32/33] apply exxport check suggestion --- matrixeater/src/com/matrixeater/gltf/GLTFExport.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index 8d1fa9bc..9f3d8c9e 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -367,9 +367,12 @@ private List getAllDoodadsPaths() { private static void export(EditableModel model, com.hiveworkshop.wc3.mdl.Animation animation, String baseDir) throws IOException { - var gltf = createGltfModel(model, animation); File outputFile = new File(baseDir + "/" + model.getName() + ".gltf"); - + if (outputFile.exists()) { + log.info("Skipping existing file: " + outputFile.getAbsolutePath()); + return; + } + var gltf = createGltfModel(model, animation); // Ensure parent directories exist File parentDir = outputFile.getParentFile(); if (parentDir != null && !parentDir.exists()) { From 5a99b74115544d03abba1c96e620bee855e8a214 Mon Sep 17 00:00:00 2001 From: Dan Costinas Date: Mon, 8 Dec 2025 23:25:11 +0200 Subject: [PATCH 33/33] Fix redundant try/catch block in actionPerformed --- .../src/com/matrixeater/gltf/GLTFExport.java | 49 ++++++++----------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java index 9f3d8c9e..fd4dbcad 100644 --- a/matrixeater/src/com/matrixeater/gltf/GLTFExport.java +++ b/matrixeater/src/com/matrixeater/gltf/GLTFExport.java @@ -248,38 +248,31 @@ public void actionPerformed(ActionEvent e) { // Doodads for (String path : doodadPaths) { - try { - var model = loadModel(path); - if (model == null) { + var model = loadModel(path); + if (model == null) { + fail++; + failedModels.add(path); + } else { + com.hiveworkshop.wc3.mdl.Animation anim = visibilityCheck.isSelected() + ? resolveAnimation.apply(model, animationField.getText()) + : null; + try { + GLTFExport.export(model, anim, "models/doodads"); + success++; + } catch (Exception ex) { + log.warning("Failed exporting doodad " + path + ": " + ex.getMessage()); fail++; failedModels.add(path); - } else { - com.hiveworkshop.wc3.mdl.Animation anim = visibilityCheck.isSelected() - ? resolveAnimation.apply(model, animationField.getText()) - : null; - try { - GLTFExport.export(model, anim, "models/doodads"); - success++; - } catch (Exception ex) { - log.warning("Failed exporting doodad " + path + ": " + ex.getMessage()); - fail++; - failedModels.add(path); - } finally { - processed++; - final int fProcessed = processed; - final int fTotal = total; - SwingUtilities.invokeLater(() -> { - exportAllProgress.setValue(fProcessed); - exportAllProgress.setString(fProcessed + " / " + fTotal); - }); - } + } finally { + processed++; + final int fProcessed = processed; + final int fTotal = total; + SwingUtilities.invokeLater(() -> { + exportAllProgress.setValue(fProcessed); + exportAllProgress.setString(fProcessed + " / " + fTotal); + }); } - } catch (Exception one) { - fail++; - failedModels.add(path); - log.warning("Failed exporting doodad " + path + ": " + one.getMessage()); } - } int finalSuccess = success;