Add textures (WIP)

This commit is contained in:
SiboVG 2023-08-19 19:43:04 +02:00
parent 765c6cde63
commit 4fc3901f00
14 changed files with 404 additions and 90 deletions

View File

@ -1498,6 +1498,8 @@ OBJOptionChooser.checkbox.removeOffset.ttip = <html>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 = <html>If true, export colors in sRGB instead of a linear color scheme.<br>Is useful for instance when exporting for use in Blender.</html>
OBJOptionChooser.lbl.LevelOfDetail = Level of detail:
OBJOptionChooser.lbl.LevelOfDetail.ttip = Select the desired level of detail of the geometry export.

View File

@ -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

View File

@ -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<DefaultMtl> materials;
private static final Logger log = LoggerFactory.getLogger(AppearanceExporter.class);
/**
* Export the appearance of a rocket component
* <b>NOTE: </b> 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<DefaultMtl> materials) {
public AppearanceExporter(DefaultObj obj, Appearance appearance, File file, OBJExportOptions options,
String materialName, List<DefaultMtl> 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);
}
}

View File

@ -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;
}
}

View File

@ -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<RocketComponent> 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<Class<? extends RocketComponent>, 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<RocketComponent> components, FlightConfiguration configuration, String filePath,
public OBJExporterFactory(List<RocketComponent> 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();
}

View File

@ -33,11 +33,12 @@ public class MassObjectExporter extends RocketComponentExporter<MassObject> {
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<MassObject> {
// 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<MassObject> {
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<MassObject> {
}
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<MassObject> {
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<MassObject> {
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<MassObject> {
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);
}

View File

@ -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);

View File

@ -119,6 +119,7 @@ public class TransitionExporter extends RocketComponentExporter<Transition> {
// 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<Transition> {
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<Transition> {
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<Transition> {
// 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<Transition> {
};
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<Integer> foreRingVertices, List<Integer> aftRingVertices,
private void addQuadVertices(int numSides, List<Integer> foreRingVertices, List<Integer> 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<Transition> {
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<Transition> {
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);
}
}

View File

@ -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<Integer> vertexList, List<Integer> 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);
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}

View File

@ -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);

View File

@ -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<ObjUtils.LevelOfDetail> LOD;
private final List<RocketComponent> 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());
}

View File

@ -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;