diff --git a/core/resources/l10n/messages.properties b/core/resources/l10n/messages.properties
index 0f129b2c2..fd8bbc143 100644
--- a/core/resources/l10n/messages.properties
+++ b/core/resources/l10n/messages.properties
@@ -1498,6 +1498,8 @@ OBJOptionChooser.checkbox.removeOffset.ttip = If true, remove the offset o
OBJOptionChooser.btn.showAdvanced = Show Advanced options
OBJOptionChooser.checkbox.triangulate = Triangulate mesh
OBJOptionChooser.checkbox.triangulate.ttip = If true, triangulate the mesh before exporting (convert all quads or high-order polygons to a triangle).
+OBJOptionChooser.checkbox.sRGB = Export colors in sRGB
+OBJOptionChooser.checkbox.sRGB.ttip = If true, export colors in sRGB instead of a linear color scheme.
Is useful for instance when exporting for use in Blender.
OBJOptionChooser.lbl.LevelOfDetail = Level of detail:
OBJOptionChooser.lbl.LevelOfDetail.ttip = Select the desired level of detail of the geometry export.
diff --git a/core/src/net/sf/openrocket/appearance/defaults/ResourceDecalImage.java b/core/src/net/sf/openrocket/appearance/defaults/ResourceDecalImage.java
index b1a53853c..9bc7a4fbe 100644
--- a/core/src/net/sf/openrocket/appearance/defaults/ResourceDecalImage.java
+++ b/core/src/net/sf/openrocket/appearance/defaults/ResourceDecalImage.java
@@ -1,11 +1,15 @@
package net.sf.openrocket.appearance.defaults;
+import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.OutputStream;
import net.sf.openrocket.appearance.DecalImage;
+import net.sf.openrocket.util.FileUtils;
import net.sf.openrocket.util.StateChangeListener;
/**
@@ -46,6 +50,14 @@ public class ResourceDecalImage implements DecalImage {
@Override
public void exportImage(File file) throws IOException {
+ InputStream is;
+ is = getBytes();
+ OutputStream os = new BufferedOutputStream(new FileOutputStream(file));
+
+ FileUtils.copy(is, os);
+
+ is.close();
+ os.close();
}
@Override
diff --git a/core/src/net/sf/openrocket/file/wavefrontobj/export/AppearanceExporter.java b/core/src/net/sf/openrocket/file/wavefrontobj/export/AppearanceExporter.java
index 4a2e0eb05..5e8236c6b 100644
--- a/core/src/net/sf/openrocket/file/wavefrontobj/export/AppearanceExporter.java
+++ b/core/src/net/sf/openrocket/file/wavefrontobj/export/AppearanceExporter.java
@@ -1,17 +1,22 @@
package net.sf.openrocket.file.wavefrontobj.export;
-import de.javagl.obj.FloatTuple;
import net.sf.openrocket.appearance.Appearance;
import net.sf.openrocket.appearance.Decal;
+import net.sf.openrocket.appearance.DecalImage;
import net.sf.openrocket.appearance.defaults.DefaultAppearance;
-import net.sf.openrocket.file.wavefrontobj.DefaultFloatTuple;
import net.sf.openrocket.file.wavefrontobj.DefaultMtl;
import net.sf.openrocket.file.wavefrontobj.DefaultObj;
import net.sf.openrocket.file.wavefrontobj.DefaultTextureOptions;
import net.sf.openrocket.rocketcomponent.RocketComponent;
import net.sf.openrocket.util.Color;
import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.List;
/**
@@ -21,21 +26,30 @@ import java.util.List;
*/
public class AppearanceExporter {
private final DefaultObj obj;
- private final RocketComponent component;
+ private final Appearance appearance;
+ private final File file;
+ private final OBJExportOptions options;
private final String materialName;
private final List materials;
+ private static final Logger log = LoggerFactory.getLogger(AppearanceExporter.class);
+
/**
* Export the appearance of a rocket component
* NOTE: you still have to call {@link #doExport()} to actually export the appearance.
* @param obj The obj file that will use the material
- * @param component The component to export the appearance of
+ * @param appearance The appearance to export
+ * @param file The file that the OBJ is exported to
+ * @param options The options to use for exporting the OBJ
* @param materialName The name of the material to generate
* @param materials The list of materials to add the new material(s) to
*/
- public AppearanceExporter(DefaultObj obj, RocketComponent component, String materialName, List materials) {
+ public AppearanceExporter(DefaultObj obj, Appearance appearance, File file, OBJExportOptions options,
+ String materialName, List materials) {
this.obj = obj;
- this.component = component;
+ this.appearance = appearance;
+ this.file = file;
+ this.options = options;
this.materialName = materialName;
this.materials = materials;
}
@@ -48,22 +62,18 @@ public class AppearanceExporter {
obj.setActiveMaterialGroupName(materialName);
DefaultMtl material = new DefaultMtl(materialName);
- // Get the component appearance
- Appearance appearance = component.getAppearance();
- if (appearance == null) {
- appearance = DefaultAppearance.getDefaultAppearance(component);
- }
-
// Apply coloring
- applyColoring(appearance, material);
+ applyColoring(appearance, material, options);
// Apply texture
applyTexture(appearance, material);
materials.add(material);
+
+ // TODO: default back to default material?
}
- private static void applyTexture(Appearance appearance, DefaultMtl material) {
+ private void applyTexture(Appearance appearance, DefaultMtl material) {
Decal texture = appearance.getTexture();
if (texture == null) {
return;
@@ -71,39 +81,74 @@ public class AppearanceExporter {
final DefaultTextureOptions textureOptions = new DefaultTextureOptions();
- // TODO: file name (save externally if saved inside .ork)
- //String filePath = texture.getImage().getDecalFile().getAbsolutePath();
- String filePath = "/Users/SiboVanGool/Downloads/hello.jpeg";
- textureOptions.setFileName(filePath);
+ // The decal file is stored inside the .ork, so first export it to the export directory
+ final File decalFile;
+ try {
+ String exportDir = file.getParent();
+ String fileName = FileUtils.removeExtension(file.getName());
+ Path decalDir = Path.of(exportDir, fileName + "_img");
+ Files.createDirectories(decalDir);
- // Texture offset
- final Coordinate origin = texture.getOffset();
- Float origX = (float) origin.x;
- Float origY = (float) origin.y;
- textureOptions.setO(origX, origY, 0f);
+ DecalImage decal = texture.getImage();
+ String decalName = FileUtils.getFileNameFromPath(decal.getName());
+ decalFile = new File(decalDir.toString(), decalName); // TODO: should name be unique?
+ decalFile.createNewFile(); // TODO: check if you want to overwrite?
+ decal.exportImage(decalFile);
+ log.info("Exported decal image to {}", decalFile.getAbsolutePath());
+ } catch (Exception e) {
+ log.error("Failed to export decal image", e);
+ return;
+ }
+
+ textureOptions.setFileName(decalFile.getAbsolutePath());
// Texture scale
final Coordinate scale = texture.getScale();
- Float scaleX = (float) scale.x;
- Float scaleY = (float) scale.y;
+ float scaleX = (float) scale.x;
+ float scaleY = (float) scale.y;
textureOptions.setS(scaleX, scaleY, 1f);
- // TODO: rotation
+ // Texture offset
+ final Coordinate origin = texture.getOffset();
+ float origX = (float) origin.x;
+ float origY = (float) (origin.y - 1 - 1/scaleY); // Need an extra offset because the texture scale origin is different in OR
+ textureOptions.setO(origX, origY, 0f);
+
+ // Texture rotation is not possible in MTL...
+
+ // Texture repeat (not very extensive in MTL...)
+ Decal.EdgeMode edgeMode = texture.getEdgeMode();
+ switch (edgeMode) {
+ case REPEAT, MIRROR -> textureOptions.setClamp(false);
+ default -> textureOptions.setClamp(true);
+ }
// Apply the texture
material.setMapKdOptions(textureOptions);
}
- private static void applyColoring(Appearance appearance, DefaultMtl material) {
+ private static void applyColoring(Appearance appearance, DefaultMtl material, OBJExportOptions options) {
Color color = appearance.getPaint();
- final float r = color.getRed()/255f;
- final float g = color.getGreen()/255f;
- final float b = color.getBlue()/255f;
+ final float r = convertColorToFloat(color.getRed(), options.isUseSRGB());
+ final float g = convertColorToFloat(color.getGreen(), options.isUseSRGB());
+ final float b = convertColorToFloat(color.getBlue(), options.isUseSRGB());
material.setKd(r, g, b); // Diffuse color
material.setKa(0f, 0f, 0f); // No emission
- material.setKs(1f, 1f, 1f); // Use white specular highlights
+ material.setKs(.25f, .25f, .25f); // Not too strong specular highlights
material.setD(color.getAlpha()/255f); // Opacity
material.setNs((float) appearance.getShine() * 750); // Shine (max is 1000, but this too strong compared to OpenRocket's max)
material.setIllum(2); // Use Phong reflection (specular highlights etc.)
}
+
+ private static float convertColorToFloat(int color, boolean sRGB) {
+ float convertedColor = color / 255f;
+ if (sRGB) {
+ convertedColor = linearTosRGB(convertedColor);
+ }
+ return convertedColor;
+ }
+
+ private static float linearTosRGB(float linear) {
+ return (float) Math.pow(linear, 2.2);
+ }
}
diff --git a/core/src/net/sf/openrocket/file/wavefrontobj/export/OBJExportOptions.java b/core/src/net/sf/openrocket/file/wavefrontobj/export/OBJExportOptions.java
index 605b1fd40..55f15713b 100644
--- a/core/src/net/sf/openrocket/file/wavefrontobj/export/OBJExportOptions.java
+++ b/core/src/net/sf/openrocket/file/wavefrontobj/export/OBJExportOptions.java
@@ -27,6 +27,10 @@ public class OBJExportOptions {
* If true, triangulate all faces (convert quads and higher-order polygons to triangles)
*/
private boolean triangulate;
+ /**
+ * If true, use sRGB colors instead of linear color space.
+ */
+ private boolean useSRGB;
/**
* The level of detail to use for the export (e.g. low-quality, normal quality...).
*/
@@ -41,8 +45,6 @@ public class OBJExportOptions {
*/
private float scaling;
- // TODO: scaling (to mm = x1000, or SI units)
-
public OBJExportOptions(Rocket rocket) {
this.exportChildren = false;
this.exportAppearance = false;
@@ -117,4 +119,12 @@ public class OBJExportOptions {
public void setScaling(float scaling) {
this.scaling = scaling;
}
+
+ public boolean isUseSRGB() {
+ return useSRGB;
+ }
+
+ public void setUseSRGB(boolean useSRGB) {
+ this.useSRGB = useSRGB;
+ }
}
diff --git a/core/src/net/sf/openrocket/file/wavefrontobj/export/OBJExporterFactory.java b/core/src/net/sf/openrocket/file/wavefrontobj/export/OBJExporterFactory.java
index cd05b8e71..14259bc81 100644
--- a/core/src/net/sf/openrocket/file/wavefrontobj/export/OBJExporterFactory.java
+++ b/core/src/net/sf/openrocket/file/wavefrontobj/export/OBJExporterFactory.java
@@ -1,6 +1,8 @@
package net.sf.openrocket.file.wavefrontobj.export;
import de.javagl.obj.ObjWriter;
+import net.sf.openrocket.appearance.Appearance;
+import net.sf.openrocket.appearance.defaults.DefaultAppearance;
import net.sf.openrocket.file.wavefrontobj.CoordTransform;
import net.sf.openrocket.file.wavefrontobj.DefaultMtl;
import net.sf.openrocket.file.wavefrontobj.DefaultMtlWriter;
@@ -16,6 +18,8 @@ import net.sf.openrocket.file.wavefrontobj.export.components.RocketComponentExpo
import net.sf.openrocket.file.wavefrontobj.export.components.RingComponentExporter;
import net.sf.openrocket.file.wavefrontobj.export.components.TransitionExporter;
import net.sf.openrocket.file.wavefrontobj.export.components.TubeFinSetExporter;
+import net.sf.openrocket.motor.Motor;
+import net.sf.openrocket.motor.MotorConfiguration;
import net.sf.openrocket.rocketcomponent.BodyTube;
import net.sf.openrocket.rocketcomponent.ComponentAssembly;
import net.sf.openrocket.rocketcomponent.FinSet;
@@ -32,6 +36,7 @@ import net.sf.openrocket.rocketcomponent.Transition;
import net.sf.openrocket.rocketcomponent.TubeFinSet;
import net.sf.openrocket.util.FileUtils;
+import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
@@ -61,7 +66,7 @@ public class OBJExporterFactory {
private final List components;
private final FlightConfiguration configuration;
private final OBJExportOptions options;
- private final String filePath;
+ private final File file;
// The different exporters for each component
private static final Map, ExporterFactory>> EXPORTER_MAP = Map.of(
@@ -81,13 +86,13 @@ public class OBJExporterFactory {
* @param components List of components to export
* @param configuration Flight configuration to use for the export
* @param options Options to use for the export
- * @param filePath Path to the file to export to
+ * @param file The file to export the OBJ to
*/
- public OBJExporterFactory(List components, FlightConfiguration configuration, String filePath,
+ public OBJExporterFactory(List components, FlightConfiguration configuration, File file,
OBJExportOptions options) {
this.components = components;
this.configuration = configuration;
- this.filePath = filePath;
+ this.file = file;
this.options = options;
}
@@ -104,7 +109,7 @@ public class OBJExporterFactory {
if (exportAsSeparateFiles) {
objFileMap = new HashMap<>();
} else {
- objFileMap = Map.of(this.filePath, obj);
+ objFileMap = Map.of(this.file.getAbsolutePath(), obj);
}
// Get all the components to export
@@ -147,7 +152,7 @@ public class OBJExporterFactory {
// If separate export, add this object to the map of objects to export
if (exportAsSeparateFiles) {
- String path = FileUtils.removeExtension(this.filePath) + "_" + groupName + ".obj";
+ String path = FileUtils.removeExtension(this.file.getAbsolutePath()) + "_" + groupName + ".obj";
objFileMap.put(path, obj);
}
@@ -223,7 +228,15 @@ public class OBJExporterFactory {
// Export material
if (options.isExportAppearance()) {
- AppearanceExporter appearanceExporter = new AppearanceExporter(obj, component, "mat_" + groupName, materials);
+ String materialName = "mat_" + groupName;
+
+ // Get the component appearance
+ Appearance appearance = component.getAppearance();
+ if (appearance == null) {
+ appearance = DefaultAppearance.getDefaultAppearance(component);
+ }
+
+ AppearanceExporter appearanceExporter = new AppearanceExporter(obj, appearance, file, options, materialName, materials);
appearanceExporter.doExport();
}
@@ -233,6 +246,19 @@ public class OBJExporterFactory {
// Export motor
if (component instanceof MotorMount) {
+ // Get the motor
+ MotorConfiguration motoConfig = ((MotorMount) component).getMotorConfig(config.getId());
+ Motor motor = motoConfig.getMotor();
+
+ // Export the motor appearance
+ if (options.isExportAppearance() && motor != null) {
+ String materialName = "mat_" + groupName + "_" + motor.getMotorName();
+ Appearance appearance = DefaultAppearance.getDefaultAppearance(motor);
+ AppearanceExporter appearanceExporter = new AppearanceExporter(obj, appearance, file, options, materialName, materials);
+ appearanceExporter.doExport();
+ }
+
+ // Export the motor geometry
MotorExporter motorExporter = new MotorExporter(obj, config, transformer, component, groupName, LOD);
motorExporter.addToObj();
}
diff --git a/core/src/net/sf/openrocket/file/wavefrontobj/export/components/MassObjectExporter.java b/core/src/net/sf/openrocket/file/wavefrontobj/export/components/MassObjectExporter.java
index c1eb0acb9..4eb2e43f3 100644
--- a/core/src/net/sf/openrocket/file/wavefrontobj/export/components/MassObjectExporter.java
+++ b/core/src/net/sf/openrocket/file/wavefrontobj/export/components/MassObjectExporter.java
@@ -33,11 +33,12 @@ public class MassObjectExporter extends RocketComponentExporter {
private void generateMesh(int numSides, int numStacks, InstanceContext context) {
// Other meshes may have been added to the obj, so we need to keep track of the starting indices
int startIdx = obj.getNumVertices();
+ int texCoordsStartIdx = obj.getNumTexCoords();
int normalsStartIdx = obj.getNumNormals();
double dx = component.getLength() / numStacks;
double da = 2.0f * Math.PI / numSides;
- // Generate vertices and normals
+ // Generate vertices, normals and UVs
for (int j = 0; j <= numStacks; j++) {
double x = j * dx;
@@ -45,6 +46,11 @@ public class MassObjectExporter extends RocketComponentExporter {
// Add a center vertex
obj.addVertex(transformer.convertLoc(x, 0, 0));
obj.addNormal(transformer.convertLocWithoutOriginOffs(j == 0 ? -1 : 1, 0, 0));
+ for (int i = 0; i <= numSides; i++) {
+ final float u = (float) i / numSides;
+ final float v = j == 0 ? 1 : 0;
+ obj.addTexCoord(u, v);
+ }
} else {
// Add a vertex for each side
for (int i = 0; i < numSides; i++) {
@@ -53,9 +59,10 @@ public class MassObjectExporter extends RocketComponentExporter {
double y = r * Math.cos(angle);
double z = r * Math.sin(angle);
+ // Vertex
obj.addVertex(transformer.convertLoc(x, y, z));
- // Add normals
+ // Normal
if (Double.compare(r, component.getRadius()) == 0) { // If in cylindrical section, use cylinder normals
obj.addNormal(transformer.convertLocWithoutOriginOffs(0, y, z));
} else {
@@ -67,13 +74,23 @@ public class MassObjectExporter extends RocketComponentExporter {
}
obj.addNormal(transformer.convertLocWithoutOriginOffs(x - xCenter, y, z)); // For smooth shading
}
+
+ // Texture coordinate
+ final float u = (float) i / numSides;
+ final float v = (float) (numStacks-j) / numStacks;
+ obj.addTexCoord(u, v);
}
+
+ // Add final UV coordinate to close the texture
+ final float u = 1f;
+ final float v = (float) (numStacks-j) / numStacks;
+ obj.addTexCoord(u, v);
}
}
int endIdx = Math.max(obj.getNumVertices() - 1, startIdx); // Clamp in case no vertices were added
- // Create bottom tip faces
+ // Create top tip faces
for (int i = 0; i < numSides; i++) {
int nextIdx = (i + 1) % numSides;
@@ -83,11 +100,17 @@ public class MassObjectExporter extends RocketComponentExporter {
1 + nextIdx,
};
int[] normalIndices = vertexIndices.clone(); // For a smooth surface, the vertex and normal indices are the same
+ int[] texCoordIndices = new int[] {
+ i,
+ numSides+1 + i,
+ numSides+1 + i+1,
+ };
ObjUtils.offsetIndex(normalIndices, normalsStartIdx);
+ ObjUtils.offsetIndex(texCoordIndices, texCoordsStartIdx);
ObjUtils.offsetIndex(vertexIndices, startIdx); // Only do this after normals are added, since the vertex indices are used for normals
- DefaultObjFace face = new DefaultObjFace(vertexIndices, null, normalIndices);
+ DefaultObjFace face = new DefaultObjFace(vertexIndices, texCoordIndices, normalIndices);
obj.addFace(face);
}
@@ -103,16 +126,23 @@ public class MassObjectExporter extends RocketComponentExporter {
1 + j * numSides + nextIdx
};
int[] normalIndices = vertexIndices.clone(); // For a smooth surface, the vertex and normal indices are the same
+ int[] texCoordIndices = new int[] {
+ j + j * numSides + i,
+ j+1 + (j + 1) * numSides + i,
+ j+1 + (j + 1) * numSides + i+1,
+ j + j * numSides + i+1
+ };
ObjUtils.offsetIndex(normalIndices, normalsStartIdx);
+ ObjUtils.offsetIndex(texCoordIndices, texCoordsStartIdx + numSides+1);
ObjUtils.offsetIndex(vertexIndices, startIdx); // Only do this after normals are added, since the vertex indices are used for normals
- DefaultObjFace face = new DefaultObjFace(vertexIndices, null, normalIndices);
+ DefaultObjFace face = new DefaultObjFace(vertexIndices, texCoordIndices, normalIndices);
obj.addFace(face);
}
}
- // Create top tip faces
+ // Create bottom tip faces
final int normalEndIdx = obj.getNumNormals() - 1;
for (int i = 0; i < numSides; i++) {
int nextIdx = (i + 1) % numSides;
@@ -126,10 +156,16 @@ public class MassObjectExporter extends RocketComponentExporter {
normalEndIdx - numSides + nextIdx,
normalEndIdx - numSides + i,
};
+ int[] texCoordIndices = new int[] {
+ numSides+1 + i,
+ i+1,
+ i,
+ };
- // Don't offset! We reference from the last index
+ ObjUtils.offsetIndex(texCoordIndices, texCoordsStartIdx + ((numStacks-1) * (numSides + 1)) );
+ // Don't offset vertices or normals! We reference from the last index
- DefaultObjFace face = new DefaultObjFace(vertexIndices, null, normalIndices);
+ DefaultObjFace face = new DefaultObjFace(vertexIndices, texCoordIndices, normalIndices);
obj.addFace(face);
}
diff --git a/core/src/net/sf/openrocket/file/wavefrontobj/export/components/MotorExporter.java b/core/src/net/sf/openrocket/file/wavefrontobj/export/components/MotorExporter.java
index 1d59d9744..e742d3152 100644
--- a/core/src/net/sf/openrocket/file/wavefrontobj/export/components/MotorExporter.java
+++ b/core/src/net/sf/openrocket/file/wavefrontobj/export/components/MotorExporter.java
@@ -56,7 +56,7 @@ public class MotorExporter {
return;
}
- obj.setActiveGroupNames(motor.getMotorName());
+ obj.setActiveGroupNames(groupName + "_" + motor.getMotorName());
for (InstanceContext context : config.getActiveInstances().getInstanceContexts(mount)) {
generateMesh(motor, context);
diff --git a/core/src/net/sf/openrocket/file/wavefrontobj/export/components/TransitionExporter.java b/core/src/net/sf/openrocket/file/wavefrontobj/export/components/TransitionExporter.java
index 6c3a2a46e..e9a100c4e 100644
--- a/core/src/net/sf/openrocket/file/wavefrontobj/export/components/TransitionExporter.java
+++ b/core/src/net/sf/openrocket/file/wavefrontobj/export/components/TransitionExporter.java
@@ -119,6 +119,7 @@ public class TransitionExporter extends RocketComponentExporter {
// Other meshes may have been added to the obj, so we need to keep track of the starting indices
final int startIdx = obj.getNumVertices();
final int normalsStartIdx = obj.getNumNormals();
+ final int texCoordsStartIdx = obj.getNumTexCoords();
final double dxBase = component.getLength() / numStacks; // Base step size in the longitudinal direction
final double actualLength = estimateActualLength(offsetRadius, dxBase); // Actual length of the transition (due to reduced step size near the fore/aft end)
@@ -176,6 +177,12 @@ public class TransitionExporter extends RocketComponentExporter {
float xTip = getTipLocation(x, xNext, offsetRadius, epsilon);
obj.addVertex(transformer.convertLoc(xTip, 0, 0));
obj.addNormal(transformer.convertLocWithoutOriginOffs(isOutside ? -1 : 1, 0, 0));
+ for (int i = 0; i <= numSides; i++) {
+ float u = (float) i / numSides;
+ float v = 1f;
+ obj.addTexCoord(u, v);
+ }
+
isForeTip = true;
// The fore tip is the first fore "ring"
@@ -218,6 +225,11 @@ public class TransitionExporter extends RocketComponentExporter {
float xTip = getTipLocation(x, xNext, offsetRadius, epsilon);
obj.addVertex(transformer.convertLoc(xTip, 0, 0));
obj.addNormal(transformer.convertLocWithoutOriginOffs(isOutside ? 1 : -1, 0, 0));
+ for (int i = 0; i <= numSides; i++) {
+ float u = (float) i / numSides;
+ float v = 0f;
+ obj.addTexCoord(u, v);
+ }
isAftTip = true;
// The aft tip is the aft "ring"
@@ -251,17 +263,20 @@ public class TransitionExporter extends RocketComponentExporter {
// Create regular faces
int corrVStartIdx = isForeTip ? startIdx + 1 : startIdx;
int corrNStartIdx = isForeTip ? normalsStartIdx + 1 : normalsStartIdx;
- addQuadFaces(numSlices, actualNumStacks, corrVStartIdx, corrNStartIdx, isOutside);
+ int corrTStartIdx = isForeTip ? texCoordsStartIdx + numSides+1 : texCoordsStartIdx;
+ addQuadFaces(numSides, actualNumStacks, corrVStartIdx, corrNStartIdx, corrTStartIdx, isOutside);
}
- private void addTipFaces(int numSlices, boolean isOutside, boolean isForeTip, int startIdx, int normalsStartIdx) {
+ private void addTipFaces(int numSides, boolean isOutside, boolean isForeTip, int startIdx, int normalsStartIdx, int texCoordsStartIdx) {
final int lastIdx = obj.getNumVertices() - 1;
- for (int i = 0; i < numSlices; i++) {
- int nextIdx = (i + 1) % numSlices;
+ for (int i = 0; i < numSides; i++) {
+ int nextIdx = (i + 1) % numSides;
int[] vertexIndices;
int[] normalIndices;
+ int[] texCoordsIndices;
// Fore tip
if (isForeTip) {
+ // Vertices
vertexIndices = new int[] {
0, // Fore tip vertex
1 + i,
@@ -269,46 +284,69 @@ public class TransitionExporter extends RocketComponentExporter {
};
vertexIndices = ObjUtils.reverseIndexWinding(vertexIndices, !isOutside);
+ // Normals
normalIndices = vertexIndices.clone(); // No need to reverse, already done by vertices
+ // Texture coordinates
+ texCoordsIndices = new int[] {
+ i,
+ numSides+1 + i,
+ numSides+1 + i+1,
+ };
+ texCoordsIndices = ObjUtils.reverseIndexWinding(texCoordsIndices, !isOutside);
+
ObjUtils.offsetIndex(normalIndices, normalsStartIdx);
+ ObjUtils.offsetIndex(texCoordsIndices, texCoordsStartIdx);
ObjUtils.offsetIndex(vertexIndices, startIdx); // Do this last, otherwise the normal indices will be wrong
}
// Aft tip
else {
+ // Vertices
vertexIndices = new int[] {
lastIdx, // Aft tip vertex
- lastIdx - numSlices + nextIdx,
- lastIdx - numSlices + i,
+ lastIdx - numSides + nextIdx,
+ lastIdx - numSides + i,
};
vertexIndices = ObjUtils.reverseIndexWinding(vertexIndices, !isOutside);
+ // Normals
int lastNormalIdx = obj.getNumNormals() - 1;
normalIndices = new int[] {
lastNormalIdx, // Aft tip vertex
- lastNormalIdx - numSlices + nextIdx,
- lastNormalIdx - numSlices + i,
+ lastNormalIdx - numSides + nextIdx,
+ lastNormalIdx - numSides + i,
};
normalIndices = ObjUtils.reverseIndexWinding(normalIndices, !isOutside);
+ // Texture coordinates
+ int lastTexCoordsIdx = obj.getNumTexCoords() - 1;
+ texCoordsIndices = new int[] {
+ lastTexCoordsIdx - (numSides+1) + i, // Aft tip vertex
+ lastTexCoordsIdx - 2*(numSides+1) + i+1,
+ lastTexCoordsIdx - 2*(numSides+1) + i,
+ };
+
// No need to offset the indices, because we reference the last vertex (this caused a lot of debugging frustration hmfmwl)
}
- DefaultObjFace face = new DefaultObjFace(vertexIndices, null, normalIndices);
+ DefaultObjFace face = new DefaultObjFace(vertexIndices, texCoordsIndices, normalIndices);
obj.addFace(face);
}
}
- private void addQuadVertices(int numSlices, List foreRingVertices, List aftRingVertices,
+ private void addQuadVertices(int numSides, List foreRingVertices, List aftRingVertices,
double r, double rNext, float x, float xNext, boolean isForeRing, boolean isAftRing, boolean isOutside) {
- for (int i = 0; i < numSlices; i++) {
- double angle = 2 * Math.PI * i / numSlices;
+ final double length = component.getLength();
+
+ for (int i = 0; i < numSides; i++) {
+ double angle = 2 * Math.PI * i / numSides;
float y = (float) (r * Math.cos(angle));
float z = (float) (r * Math.sin(angle));
+ // Vertex
obj.addVertex(transformer.convertLoc(x, y, z));
- // Add the ring vertices to the lists
+ //// Add the ring vertices to the lists
if (isForeRing) {
foreRingVertices.add(obj.getNumVertices()-1);
}
@@ -316,8 +354,8 @@ public class TransitionExporter extends RocketComponentExporter {
aftRingVertices.add(obj.getNumVertices()-1);
}
- // Calculate the normal
- // We need special nx normal when the radius changes
+ // Normal
+ //// We need special nx normal when the radius changes
float nx;
if (Double.compare(r, rNext) != 0) {
final double slopeAngle = Math.atan(Math.abs(xNext - x) / (rNext - r));
@@ -332,27 +370,49 @@ public class TransitionExporter extends RocketComponentExporter {
ny = isOutside ? ny : -ny;
nz = isOutside ? nz : -nz;
obj.addNormal(transformer.convertLocWithoutOriginOffs(nx, ny, nz));
+
+ // Texture coordinates
+ float u = (float) i / numSides;
+ float v = (float) ((length-x) / length);
+ obj.addTexCoord(u, v);
}
+
+ // Need to add a last texture coordinate to close the texture
+ final float v = (float) ((length-x) / length);
+ obj.addTexCoord(1f, v);
}
- private void addQuadFaces(int numSlices, int numStacks, int startIdx, int normalsStartIdx, boolean isOutside) {
+ private void addQuadFaces(int numSides, int numStacks, int startIdx, int normalsStartIdx, int texCoordsStartIdx, boolean isOutside) {
for (int i = 0; i < numStacks - 1; i++) {
- for (int j = 0; j < numSlices; j++) {
- final int nextIdx = (j + 1) % numSlices;
+ for (int j = 0; j < numSides; j++) {
+ final int nextIdx = (j + 1) % numSides;
+
+ // Vertices
int[] vertexIndices = new int[] {
- i * numSlices + j, // Bottom-left of quad
- (i + 1) * numSlices + j, // Top-left of quad
- (i + 1) * numSlices + nextIdx, // Top-right of quad
- i * numSlices + nextIdx, // Bottom-right of quad
+ i * numSides + j, // Bottom-left of quad
+ (i + 1) * numSides + j, // Top-left of quad
+ (i + 1) * numSides + nextIdx, // Top-right of quad
+ i * numSides + nextIdx, // Bottom-right of quad
};
vertexIndices = ObjUtils.reverseIndexWinding(vertexIndices, !isOutside);
+ // Normals
int[] normalIndices = vertexIndices.clone(); // No reversing needed, already done by vertices
+ // Texture coordinates
+ int[] texCoordsIndices = new int[] {
+ i * (numSides+1) + j,
+ (i + 1) * (numSides+1) + j,
+ (i + 1) * (numSides+1) + j+1,
+ i * (numSides+1) + j+1,
+ };
+ texCoordsIndices = ObjUtils.reverseIndexWinding(texCoordsIndices, !isOutside);
+
ObjUtils.offsetIndex(normalIndices, normalsStartIdx);
+ ObjUtils.offsetIndex(texCoordsIndices, texCoordsStartIdx);
ObjUtils.offsetIndex(vertexIndices, startIdx); // Do this last, otherwise the normal indices will be wrong
- DefaultObjFace face = new DefaultObjFace(vertexIndices, null, normalIndices);
+ DefaultObjFace face = new DefaultObjFace(vertexIndices, texCoordsIndices, normalIndices);
obj.addFace(face);
}
}
diff --git a/core/src/net/sf/openrocket/file/wavefrontobj/export/shapes/CylinderExporter.java b/core/src/net/sf/openrocket/file/wavefrontobj/export/shapes/CylinderExporter.java
index 2e6926cfe..03ed31936 100644
--- a/core/src/net/sf/openrocket/file/wavefrontobj/export/shapes/CylinderExporter.java
+++ b/core/src/net/sf/openrocket/file/wavefrontobj/export/shapes/CylinderExporter.java
@@ -38,6 +38,7 @@ public class CylinderExporter {
// Other meshes may have been added to the obj, so we need to keep track of the starting indices
int startIdx = obj.getNumVertices();
+ int texCoordsStartIdx = obj.getNumTexCoords();
int normalsStartIdx = obj.getNumNormals();
if (solid) {
@@ -51,16 +52,18 @@ public class CylinderExporter {
}
// Generate side top vertices
- generateRingVertices(obj, transformer, numSides, 0, length, foreRadius, aftRadius, isOutside, foreRingVertices, foreRingNormals);
+ generateRingVertices(obj, transformer, numSides, 0, length, length, foreRadius, aftRadius, isOutside, foreRingVertices, foreRingNormals);
// Generate side bottom vertices
- generateRingVertices(obj, transformer, numSides, length, 0, aftRadius, foreRadius, isOutside, aftRingVertices, aftRingNormals);
+ generateRingVertices(obj, transformer, numSides, length, 0, length, aftRadius, foreRadius, isOutside, aftRingVertices, aftRingNormals);
// Create faces for the bottom and top
if (solid) {
for (int i = 0; i < numSides; i++) {
int nextIdx = (i + 1) % numSides;
+
// Bottom face
+ //// Vertices
int[] vertexIndices = new int[] {
0, // Bottom center vertex
2 + i,
@@ -69,13 +72,18 @@ public class CylinderExporter {
vertexIndices = ObjUtils.reverseIndexWinding(vertexIndices, !isOutside);
ObjUtils.offsetIndex(vertexIndices, startIdx);
+ //// Normals
int[] normalIndices = new int[]{0, 0, 0};
ObjUtils.offsetIndex(normalIndices, normalsStartIdx);
+ //// Texture coordinates
+ // TODO?
+
DefaultObjFace face = new DefaultObjFace(vertexIndices, null, normalIndices);
obj.addFace(face);
// Top face
+ //// Vertices
vertexIndices = new int[] {
1, // Top center vertex
2 + numSides + ((i + 1) % numSides),
@@ -84,9 +92,13 @@ public class CylinderExporter {
vertexIndices = ObjUtils.reverseIndexWinding(vertexIndices, !isOutside);
ObjUtils.offsetIndex(vertexIndices, startIdx);
+ //// Normals
normalIndices = new int[] {1, 1, 1};
ObjUtils.offsetIndex(normalIndices, normalsStartIdx);
+ //// Texture coordinates
+ // TODO
+
face = new DefaultObjFace(vertexIndices, null, normalIndices);
obj.addFace(face);
}
@@ -97,6 +109,7 @@ public class CylinderExporter {
final int nextIdx = (i + 1) % numSides;
final int offset = solid ? 2 : 0; // Offset by 2 to skip the bottom and top center vertices
+ //// Vertices
int[] vertexIndices = new int[]{
i, // Bottom-left of quad
numSides + i, // Top-left of quad
@@ -105,12 +118,23 @@ public class CylinderExporter {
};
vertexIndices = ObjUtils.reverseIndexWinding(vertexIndices, !isOutside);
+ //// Normals
int[] normalIndices = vertexIndices.clone(); // No need to reverse winding, already done by vertices
+ //// Texture coordinates
+ int[] texCoordIndices = new int[]{
+ i, // Bottom-left of quad
+ numSides+1 + i, // Top-left of quad
+ numSides+1 + i+1, // Top-right of quad (don't use nextIdx, we don't want roll-over)
+ i+1, // Bottom-right of quad (don't use nextIdx, we don't want roll-over)
+ };
+ texCoordIndices = ObjUtils.reverseIndexWinding(texCoordIndices, !isOutside);
+
ObjUtils.offsetIndex(normalIndices, normalsStartIdx + offset);
+ ObjUtils.offsetIndex(texCoordIndices, texCoordsStartIdx);
ObjUtils.offsetIndex(vertexIndices, startIdx + offset); // ! Only add offset here, otherwise you mess up the indices for the normals
- DefaultObjFace face = new DefaultObjFace(vertexIndices, null, normalIndices);
+ DefaultObjFace face = new DefaultObjFace(vertexIndices, texCoordIndices, normalIndices);
obj.addFace(face);
}
}
@@ -147,7 +171,7 @@ public class CylinderExporter {
}
public static void generateRingVertices(DefaultObj obj, CoordTransform transformer,
- int numSides, float x, float nextX, float radius, float nextRadius,
+ int numSides, float x, float nextX, float xMax, float radius, float nextRadius,
boolean isOutside, List vertexList, List normalList) {
int startIdx = obj.getNumVertices();
int normalsStartIdx = obj.getNumNormals();
@@ -183,6 +207,15 @@ public class CylinderExporter {
if (normalList != null) {
normalList.add(normalsStartIdx + i);
}
+
+ // Texture coordinates
+ final float u = (float) i / numSides;
+ final float v = isOutside ? (xMax - x) / xMax : x / xMax; // For some reason, the texture is vertically flipped in OR for inside cylinders. Don't really like it, but it is what it is
+ obj.addTexCoord(u, v);
}
+
+ // Need to add a last texture coordinate for the end of the texture
+ final float v = isOutside ? (xMax - x) / xMax : x / xMax;
+ obj.addTexCoord(1f, v);
}
}
diff --git a/core/src/net/sf/openrocket/file/wavefrontobj/export/shapes/PolygonExporter.java b/core/src/net/sf/openrocket/file/wavefrontobj/export/shapes/PolygonExporter.java
index a25110105..e380b3408 100644
--- a/core/src/net/sf/openrocket/file/wavefrontobj/export/shapes/PolygonExporter.java
+++ b/core/src/net/sf/openrocket/file/wavefrontobj/export/shapes/PolygonExporter.java
@@ -32,39 +32,60 @@ public class PolygonExporter {
// NOTE: "front" is the side view with the normal pointing towards the viewer, "back" is the side with the normal pointing away from the viewer
+ // Calculate the boundaries of the polygon
+ Boundaries boundaries = new Boundaries(pointLocationsX, pointLocationsY);
+
obj.addNormal(transformer.convertLocWithoutOriginOffs(0, 0, -1)); // Front faces normal
obj.addNormal(transformer.convertLocWithoutOriginOffs(0, 0, 1)); // Back faces normal
// Generate front face vertices
for (int i = 0; i < pointLocationsX.length; i++) {
obj.addVertex(transformer.convertLoc(pointLocationsX[i], pointLocationsY[i], -thickness/2));
+
+ // Compute texture coordinates based on normalized position
+ float u = (pointLocationsX[i] - boundaries.getMinX()) / (boundaries.getMaxX() - boundaries.getMinX());
+ u = 1f - u;
+ float v = (pointLocationsY[i] - boundaries.getMinY()) / (boundaries.getMaxY() - boundaries.getMinY());
+ v = 1f - v;
+ obj.addTexCoord(u, v);
}
// Generate back face vertices
for (int i = 0; i < pointLocationsX.length; i++) {
obj.addVertex(transformer.convertLoc(pointLocationsX[i], pointLocationsY[i], thickness/2));
+
+ // Compute texture coordinates based on normalized position
+ float u = (pointLocationsX[i] - boundaries.getMinX()) / (boundaries.getMaxX() - boundaries.getMinX());
+ u = 1f - u;
+ float v = (pointLocationsY[i] - boundaries.getMinY()) / (boundaries.getMaxY() - boundaries.getMinY());
+ v = 1f - v;
+ obj.addTexCoord(u, v);
}
// Create front face
int[] vertexIndices = new int[pointLocationsX.length];
int[] normalIndices = new int[pointLocationsX.length];
+ int[] texCoordsIndices = new int[pointLocationsX.length];
for (int i = 0; i < pointLocationsX.length; i++) {
vertexIndices[i] = pointLocationsX.length-1 - i;
normalIndices[i] = normalsStartIdx;
+ texCoordsIndices[i] = pointLocationsX.length-1 - i;
}
ObjUtils.offsetIndex(vertexIndices, startIdx);
- DefaultObjFace face = new DefaultObjFace(vertexIndices, null, normalIndices);
+ DefaultObjFace face = new DefaultObjFace(vertexIndices, texCoordsIndices, normalIndices);
obj.addFace(face);
// Create back face
vertexIndices = new int[pointLocationsX.length];
normalIndices = new int[pointLocationsX.length];
+ texCoordsIndices = new int[pointLocationsX.length];
for (int i = 0; i < pointLocationsX.length; i++) {
vertexIndices[i] = pointLocationsX.length + i;
normalIndices[i] = normalsStartIdx + 1;
+ texCoordsIndices[i] = pointLocationsX.length + i;
}
ObjUtils.offsetIndex(vertexIndices, startIdx);
- face = new DefaultObjFace(vertexIndices, null, normalIndices);
+ face = new DefaultObjFace(vertexIndices, texCoordsIndices, normalIndices);
obj.addFace(face);
// Create side faces
@@ -111,4 +132,55 @@ public class PolygonExporter {
throw new IllegalArgumentException("The first and last points must be different");
}
}
+
+ /**
+ * Calculate the boundaries of a polygon.
+ */
+ private static class Boundaries {
+ private float minX;
+ private float maxX;
+ private float minY;
+ private float maxY;
+
+ public Boundaries(float[] pointsX, float[] pointsY) {
+ this.minX = Float.MAX_VALUE;
+ this.maxX = Float.MIN_VALUE;
+ this.minY = Float.MAX_VALUE;
+ this.maxY = Float.MIN_VALUE;
+
+ for (int i = 0; i < pointsX.length; i++) {
+ float x = pointsX[i];
+ float y = pointsY[i];
+
+ if (x < minX) {
+ minX = x;
+ }
+ if (x > maxX) {
+ maxX = x;
+ }
+ if (y < minY) {
+ minY = y;
+ }
+ if (y > maxY) {
+ maxY = y;
+ }
+ }
+ }
+
+ public float getMinX() {
+ return minX;
+ }
+
+ public float getMaxX() {
+ return maxX;
+ }
+
+ public float getMinY() {
+ return minY;
+ }
+
+ public float getMaxY() {
+ return maxY;
+ }
+ }
}
diff --git a/core/src/net/sf/openrocket/util/FileUtils.java b/core/src/net/sf/openrocket/util/FileUtils.java
index 5cfeb3321..09b77bfdc 100644
--- a/core/src/net/sf/openrocket/util/FileUtils.java
+++ b/core/src/net/sf/openrocket/util/FileUtils.java
@@ -6,6 +6,7 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.nio.file.Paths;
public abstract class FileUtils {
@@ -51,4 +52,13 @@ public abstract class FileUtils {
return fileName;
}
+ /**
+ * Get the file name from a path.
+ * @param pathString the path (e.g. "my/file/path.txt")
+ * @return the file name (e.g. "path.txt")
+ */
+ public static String getFileNameFromPath(String pathString) {
+ return Paths.get(pathString).getFileName().toString();
+ }
+
}
diff --git a/core/test/net/sf/openrocket/file/wavefrontobj/export/OBJExporterFactoryTest.java b/core/test/net/sf/openrocket/file/wavefrontobj/export/OBJExporterFactoryTest.java
index 8a087066c..2c550027c 100644
--- a/core/test/net/sf/openrocket/file/wavefrontobj/export/OBJExporterFactoryTest.java
+++ b/core/test/net/sf/openrocket/file/wavefrontobj/export/OBJExporterFactoryTest.java
@@ -15,6 +15,7 @@ import net.sf.openrocket.file.RocketLoadException;
import net.sf.openrocket.file.openrocket.OpenRocketSaverTest;
import net.sf.openrocket.file.wavefrontobj.CoordTransform;
import net.sf.openrocket.file.wavefrontobj.DefaultCoordTransform;
+import net.sf.openrocket.file.wavefrontobj.ObjUtils;
import net.sf.openrocket.l10n.DebugTranslator;
import net.sf.openrocket.l10n.Translator;
import net.sf.openrocket.plugin.PluginModule;
@@ -158,29 +159,29 @@ public class OBJExporterFactoryTest {
// Create a temp file for storing the exported OBJ
Path tempFile = Files.createTempFile("testExport", ".obj");
- String filePath = tempFile.toAbsolutePath().toString();
-
-
- filePath = "/Users/SiboVanGool/Downloads/testExport.obj"; // TODO: remove this line
-
// Do the exporting
OBJExportOptions options = new OBJExportOptions(rocket);
options.setScaling(30);
options.setExportChildren(true);
- OBJExporterFactory exporterFactory = new OBJExporterFactory(components, rocket.getSelectedConfiguration(), filePath, options);
+ options.setExportAppearance(true);
+ options.setRemoveOffset(true);
+ OBJExporterFactory exporterFactory = new OBJExporterFactory(components, rocket.getSelectedConfiguration(), tempFile.toFile(), options);
exporterFactory.doExport();
// Test with other parameters
- /*noseCone.setShoulderCapped(false);
+ noseCone.setShoulderCapped(false);
railButton.setScrewHeight(0);
bodyTube.setFilled(true);
- transformer = new DefaultCoordTransform(rocket.getLength());
- exporterFactory = new OBJExporterFactory(components, rocket.getSelectedConfiguration(), true, false, false,
- transformer, filePath);
- exporterFactory.doExport();*/
+ options.setTriangulate(true);
+ options.setRemoveOffset(false);
+ options.setScaling(1000);
+ options.setLOD(ObjUtils.LevelOfDetail.LOW_QUALITY);
+
+ exporterFactory = new OBJExporterFactory(components, rocket.getSelectedConfiguration(), tempFile.toFile(), options);
+ exporterFactory.doExport();
// Clean up
Files.delete(tempFile);
diff --git a/swing/src/net/sf/openrocket/file/wavefrontobj/OBJOptionChooser.java b/swing/src/net/sf/openrocket/file/wavefrontobj/OBJOptionChooser.java
index 048af06e1..0b1c4c872 100644
--- a/swing/src/net/sf/openrocket/file/wavefrontobj/OBJOptionChooser.java
+++ b/swing/src/net/sf/openrocket/file/wavefrontobj/OBJOptionChooser.java
@@ -26,6 +26,7 @@ public class OBJOptionChooser extends JPanel {
private final JCheckBox exportAsSeparateFiles;
private final JCheckBox removeOffset;
private final JCheckBox triangulate;
+ private final JCheckBox sRGB;
private final JComboBox LOD;
private final List selectedComponents;
@@ -73,6 +74,10 @@ public class OBJOptionChooser extends JPanel {
this.triangulate.setToolTipText(trans.get("OBJOptionChooser.checkbox.triangulate.ttip"));
advancedOptionsPanel.add(triangulate, "spanx, wrap");
+ //// Export colors in sRGB
+ this.sRGB = new JCheckBox(trans.get("OBJOptionChooser.checkbox.sRGB"));
+ this.sRGB.setToolTipText(trans.get("OBJOptionChooser.checkbox.sRGB.ttip"));
+ advancedOptionsPanel.add(sRGB, "spanx, wrap");
//// Level of detail
JLabel LODLabel = new JLabel(trans.get("OBJOptionChooser.lbl.LevelOfDetail"));
@@ -124,6 +129,7 @@ public class OBJOptionChooser extends JPanel {
this.exportAsSeparateFiles.setSelected(opts.isExportAsSeparateFiles());
this.removeOffset.setSelected(opts.isRemoveOffset());
this.triangulate.setSelected(opts.isTriangulate());
+ this.sRGB.setSelected(opts.isUseSRGB());
this.LOD.setSelectedItem(opts.getLOD());
}
@@ -144,6 +150,7 @@ public class OBJOptionChooser extends JPanel {
opts.setExportAsSeparateFiles(exportAsSeparateFiles.isSelected());
opts.setRemoveOffset(removeOffset.isSelected());
opts.setTriangulate(triangulate.isSelected());
+ opts.setUseSRGB(sRGB.isSelected());
opts.setLOD((ObjUtils.LevelOfDetail) LOD.getSelectedItem());
}
diff --git a/swing/src/net/sf/openrocket/gui/main/BasicFrame.java b/swing/src/net/sf/openrocket/gui/main/BasicFrame.java
index 508a9a3c5..08870e1ab 100644
--- a/swing/src/net/sf/openrocket/gui/main/BasicFrame.java
+++ b/swing/src/net/sf/openrocket/gui/main/BasicFrame.java
@@ -1678,7 +1678,7 @@ public class BasicFrame extends JFrame {
*/
private boolean saveWavefrontOBJFile(File file, OBJExportOptions options) {
OBJExporterFactory exporter = new OBJExporterFactory(getSelectedComponents(), rocket.getSelectedConfiguration(),
- file.getAbsolutePath(), options);
+ file, options);
exporter.doExport();
return true;