I face some weird behavior after adding 3d content to my JavaFX application which contains a toolbar.
I created a small project that replicates this problem. In the main application, I use SubScene but the behavior is absolutely the same.
Here is what the application should look like(without toolbar)
After adding this single line
ToolBar toolBar = new ToolBar();
The content becomes like this
Here is the code
import java.util.*;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.DepthTest;
import javafx.scene.Group;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.control.ToolBar;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.CullFace;
import javafx.scene.shape.DrawMode;
import javafx.scene.shape.Line;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.TriangleMesh;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
import javafx.util.Pair;
import org.apache.commons.math3.analysis.interpolation.PiecewiseBicubicSplineInterpolatingFunction;
public class Main extends Application {
double[] xMetricData = {1 * 1000, 10 * 1000, 20 * 1000, 30 * 1000, 40 * 1000};
double[] yMetricData = {1 * 1000, 10 * 1000, 20 * 1000, 30 * 1000, 40 * 1000, 50 * 1000};
double[][] fMetricData = {{25, 5, 10, 15, 30, 30}, {15, -5, 5, 15, 25, 25}, {35, 5, 15, 10, 15, 15}, {15, 25, 15, 25, 35, 35}, {20, 30, 25, 30, 40, 40}};
int size = 50;
int cubeSize = 100;
// variables for mouse interaction
private double mousePosX, mousePosY;
private double mouseOldX, mouseOldY;
private final Rotate rotateX = new Rotate(20, Rotate.X_AXIS);
private final Rotate rotateY = new Rotate(-45, Rotate.Y_AXIS);
#Override
public void start(Stage primaryStage) {
// create axis walls
Group cube = createCube(cubeSize);
// initial cube rotation
cube.getTransforms().addAll(rotateX, rotateY);
//ToolBar toolBar = new ToolBar();
// add objects to scene
StackPane root = new StackPane();
root.getChildren().add(cube);
TriangleMesh mesh = new TriangleMesh();
int xLength = 0, zLength = 0;
for (int x = (int)xMetricData[0]; x < xMetricData[xMetricData.length - 1]; x+= 1000)
{
zLength = 0;
for (int z = (int) yMetricData[0]; z < yMetricData[yMetricData.length - 1]; z+= 1000)
{
float y = (float) apacheInterpolation(x, z);
//System.out.println(x +"," + z + ", " + y);
mesh.getPoints().addAll(x / 1000.0f, y, z / 1000.0f);
zLength += 1;
}
xLength += 1;
}
for (float x = 0; x < xLength - 1; x++)
{
for (float y = 0; y < zLength - 1; y++)
{
float x0 = x / xLength;
float y0 = y / zLength;
float x1 = (x + 1) / xLength;
float y1 = (y + 1) / zLength;
mesh.getTexCoords().addAll( //
x0, y0, // 0, top-left
x0, y1, // 1, bottom-left
x1, y1, // 2, top-right
x1, y1 // 3, bottom-right
);
}
}
// faces
for (int x = 0; x < xLength - 1; x++)
{
for (int z = 0; z < zLength - 1; z++)
{
int tl = x * zLength + z; // top-left
int bl = x * zLength + z + 1; // bottom-left
int tr = (x + 1) * zLength + z; // top-right
int br = (x + 1) * zLength + z + 1; // bottom-right
int offset = (x * (zLength - 1) + z ) * 8 / 2; // div 2 because we have u AND v in the list
// working
mesh.getFaces().addAll(bl, offset + 1, tl, offset + 0, tr, offset + 2);
mesh.getFaces().addAll(tr, offset + 2, br, offset + 3, bl, offset + 1);
System.out.println(tl +", " + tr);
}
}
System.out.println(mesh.getPoints().size());
PhongMaterial material = new PhongMaterial();
//material.setDiffuseMap(diffuseMap);
material.setSpecularColor(Color.BLACK);
MeshView meshView = new MeshView(mesh);
// meshView.setTranslateX(-0.5 * size);
// meshView.setTranslateZ(-0.5 * size);
meshView.setMaterial(material);
meshView.setCullFace(CullFace.BACK);
meshView.setDrawMode(DrawMode.FILL);
meshView.setDepthTest(DepthTest.ENABLE);
cube.getChildren().addAll(meshView);
// scene
Scene scene = new Scene(root, 800, 700, true, SceneAntialiasing.BALANCED);
scene.setCamera(new PerspectiveCamera());
scene.setOnMousePressed(me -> {
mouseOldX = me.getSceneX();
mouseOldY = me.getSceneY();
});
scene.setOnMouseDragged(me -> {
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
rotateX.setAngle(rotateX.getAngle() - (mousePosY - mouseOldY));
rotateY.setAngle(rotateY.getAngle() + (mousePosX - mouseOldX));
mouseOldX = mousePosX;
mouseOldY = mousePosY;
});
makeZoomable(root);
primaryStage.setResizable(true);
primaryStage.setScene(scene);
primaryStage.show();
}
public static double getFirmwareOffset(List<int[]> matrix, int x, int y)
{
Comparator<Pair<Integer, Long>> idComparator = (p1, p2) -> (int) (p2.getKey() - p1.getValue());
Queue<Pair<Integer, Long>> integerPriorityQueue = new PriorityQueue<>(5, idComparator);
long minDistance = -1;
long sumDistance = 0;
for (int i = 0; i < matrix.size(); ++i)
{
long difX = (long)Math.abs(matrix.get(i)[0] - x);
long difY = (long)Math.abs(matrix.get(i)[1] - y);
// in case accuracy is not enough we coudl go back to the traditional formula
//long distance = sqrt(difX * difX + difY * difY);
long distance = difX + difY;
if (distance == 0)
return matrix.get(i)[2];
if (integerPriorityQueue.size() < 15)
{
integerPriorityQueue.add(new Pair<>(i, distance));
sumDistance += distance;
}
else
{
Pair<Integer, Long> top = integerPriorityQueue.peek();
if (top.getValue() > distance)
{
integerPriorityQueue.poll();
integerPriorityQueue.add(new Pair<>(i, distance));
sumDistance -= top.getValue();
sumDistance += distance;
}
}
if (minDistance == -1 || distance < minDistance)
minDistance = distance;
}
double sumRatios = 0;
List<Pair<Integer, Double>> ratios = new ArrayList<>();
while(integerPriorityQueue.size() > 0)
{
Pair<Integer, Long> entry = integerPriorityQueue.poll();
double ratio = (double)sumDistance / entry.getValue();
ratios.add(new Pair<>(entry.getKey(), ratio));
sumRatios += ratio;
}
if (sumRatios == 0)
return 0;
double offset = 0;
for (int i = 0; i < ratios.size(); ++i)
{
Pair<Integer, Double> entry = ratios.get(i);
offset += (matrix.get(entry.getKey())[2] * entry.getValue() / sumRatios);
}
return offset;
}
/**
* Axis wall
*/
public static class Axis extends Pane {
Rectangle wall;
public Axis(double size) {
// wall
// first the wall, then the lines => overlapping of lines over walls
// works
wall = new Rectangle(size, size);
getChildren().add(wall);
// grid
double zTranslate = 0;
double lineWidth = 1.0;
Color gridColor = Color.WHITE;
for (int y = 0; y <= size; y += size / 10) {
Line line = new Line(0, 0, size, 0);
line.setStroke(gridColor);
line.setFill(gridColor);
line.setTranslateY(y);
line.setTranslateZ(zTranslate);
line.setStrokeWidth(lineWidth);
getChildren().addAll(line);
}
for (int x = 0; x <= size; x += size / 10) {
Line line = new Line(0, 0, 0, size);
line.setStroke(gridColor);
line.setFill(gridColor);
line.setTranslateX(x);
line.setTranslateZ(zTranslate);
line.setStrokeWidth(lineWidth);
getChildren().addAll(line);
}
// labels
// TODO: for some reason the text makes the wall have an offset
// for( int y=0; y <= size; y+=size/10) {
//
// Text text = new Text( ""+y);
// text.setTranslateX(size + 10);
//
// text.setTranslateY(y);
// text.setTranslateZ(zTranslate);
//
// getChildren().addAll(text);
//
// }
}
public void setFill(Paint paint) {
wall.setFill(paint);
}
}
public void makeZoomable(StackPane control) {
final double MAX_SCALE = 20.0;
final double MIN_SCALE = 4;
control.setScaleX(MIN_SCALE);
control.setScaleY(MIN_SCALE);
control.addEventFilter(ScrollEvent.ANY, new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
double delta = 1.2;
double scale = control.getScaleX();
if (event.getDeltaY() < 0) {
scale /= delta;
} else {
scale *= delta;
}
scale = clamp(scale, MIN_SCALE, MAX_SCALE);
control.setScaleX(scale);
control.setScaleY(scale);
event.consume();
}
});
}
/**
* Create axis walls
* #param size
* #return
*/
private Group createCube(int size) {
Group cube = new Group();
// size of the cube
Color color = Color.DARKCYAN;
List<Axis> cubeFaces = new ArrayList<>();
Axis r;
// back face
r = new Axis(size);
r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.5 * 1), 1.0));
r.setTranslateX(-0.5 * size);
r.setTranslateY(-0.5 * size);
r.setTranslateZ(0.5 * size);
cubeFaces.add(r);
// bottom face
r = new Axis(size);
r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.4 * 1), 1.0));
r.setTranslateX(-0.5 * size);
r.setTranslateY(0);
r.setRotationAxis(Rotate.X_AXIS);
r.setRotate(90);
cubeFaces.add(r);
// right face
r = new Axis(size);
r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.3 * 1), 1.0));
r.setTranslateX(-1 * size);
r.setTranslateY(-0.5 * size);
r.setRotationAxis(Rotate.Y_AXIS);
r.setRotate(90);
// cubeFaces.add( r);
// left face
r = new Axis(size);
r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.2 * 1), 1.0));
r.setTranslateX(0);
r.setTranslateY(-0.5 * size);
r.setRotationAxis(Rotate.Y_AXIS);
r.setRotate(90);
cubeFaces.add(r);
// top face
r = new Axis(size);
r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.1 * 1), 1.0));
r.setTranslateX(-0.5 * size);
r.setTranslateY(-1 * size);
r.setRotationAxis(Rotate.X_AXIS);
r.setRotate(90);
// cubeFaces.add( r);
// front face
r = new Axis(size);
r.setFill(color.deriveColor(0.0, 1.0, (1 - 0.1 * 1), 1.0));
r.setTranslateX(-0.5 * size);
r.setTranslateY(-0.5 * size);
r.setTranslateZ(-0.5 * size);
// cubeFaces.add( r);
cube.getChildren().addAll(cubeFaces);
return cube;
}
public static double normalizeValue(double value, double min, double max, double newMin, double newMax) {
return (value - min) * (newMax - newMin) / (max - min) + newMin;
}
public static double clamp(double value, double min, double max) {
if (Double.compare(value, min) < 0)
return min;
if (Double.compare(value, max) > 0)
return max;
return value;
}
public static void main(String[] args) {
launch(args);
}
public double apacheInterpolation(float px, float py)
{
PiecewiseBicubicSplineInterpolatingFunction t = new PiecewiseBicubicSplineInterpolatingFunction(xMetricData, yMetricData, fMetricData);
return t.value(px, py);
}
}
UPD: After a bit investigation, any control leads to this behavior.
Use a SubScene and adjust the nearClip of the camera to the smallest possible value:
SubScene scene = new SubScene(root, 800, 700, true, SceneAntialiasing.BALANCED);
PerspectiveCamera camera = new PerspectiveCamera();
camera.setNearClip(Double.MIN_VALUE);
scene.setCamera(camera);
...
BorderPane sRoot = new BorderPane(scene);
sRoot.setTop(new ToolBar());
Scene mainScene = new Scene(sRoot);
primaryStage.setResizable(true);
primaryStage.setScene(mainScene);
primaryStage.show();
As #Fabian mentioned I continued experimenting with SubScene. The trick is not to use any Panes while adding 3d elements to a SubScene.
Related
I've looked at several examples of people creating tile maps, and I am unable to get the tile position where my mouse is pointed at.
I am using a spritebatch and GameTile[][] to create the map. Keep in mind that the tiles themselves are isometric and not actually a square.
The method renderMap() is where the map is actually is being rendered. createMap() just sets the initial GameTiles for an empty map.
The map is able to be dragged and zoomed in and out using Ortho camera.
Zooming out gives me an issue as well, the tiles seem to be shifted over on click
public class MapEditor implements GameScene {
private GameContext context;
private SpriteBatch batch;
private OrthographicCamera camera;
public static GameTile[][] tiles; //GameTile.WIDTH = 64 & GameTile.HEIGHT =48
public static final int MAP_WIDTH = 20;
public static final int MAP_HEIGHT = 36;
public MapEditor(GameContext context) {
this.context = context;
tiles = new GameTile[MAP_WIDTH][MAP_HEIGHT];
}
#Override
public void create() {
renderer = new ShapeRenderer();
this.batch = new SpriteBatch();
camera = new OrthographicCamera(Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
}
public void createMap() {
// Create the sea tiles
for (int x = 0; x < MAP_WIDTH; x++) {
for (int y = 0; y < MAP_HEIGHT; y++) {
if (y < 3 || y > 32) {
if(tiles[x][y] == null) {
tiles[x][y] = safezone;
}
}
else {
if(tiles[x][y] == null) {
tiles[x][y] = cell;
}
}
}
}
}
#Override
public void update(){
// update the camera
camera.update();
}
#Override
public void render() {
batch.setProjectionMatrix(camera.combined);
batch.begin();
Gdx.gl.glViewport(0,0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
renderMap();
batch.end();
}
public int getTileX(float x, float y) {
/*
* getRegionWidth() = TILE_WIDTH_HALF
* getRegionHeight() = TILE_HEIGHT_HALF
* these are the ones being added to worldCoords.x/y
*/
Vector3 worldCoords = camera.unproject(new Vector3(x, y, 0));
return (int)((TILE_WIDTH_HALF * ((-TILE_HEIGHT_HALF + (worldCoords.y + TILE_HEIGHT_HALF)) /
TILE_HEIGHT_HALF) + (worldCoords.x + TILE_WIDTH_HALF)) / TILE_WIDTH_HALF) / 2;
}
public int getTileY(float x, float y) {
/*
* getRegionWidth() = TILE_WIDTH_HALF
* getRegionHeight() = TILE_HEIGHT_HALF
* these are the ones being added to worldCoords.x/y
*/
Vector3 worldCoords = camera.unproject(new Vector3(x, y, 0));
return (int)(((-TILE_HEIGHT_HALF * (TILE_WIDTH_HALF + (worldCoords.x + TILE_WIDTH_HALF)) /
TILE_WIDTH_HALF) + (worldCoords.y + TILE_HEIGHT_HALF)) / TILE_HEIGHT_HALF) / 2;
}
#Override
public boolean handleClick(float x, float y, int button) {
int tileX = getTileX(x,y);
int tileY = getTileY(x,y);
System.out.println("Tile:"+tileX + ","+tileY);
}
private void renderMap() {
for (int i = 0; i < tiles.length; i++) {
for(int j = 0; j < tiles[i].length; j++) {
TextureRegion region = tiles[i][j].getRegion();
int x = (i * GameTile.TILE_WIDTH / 2) - (j * GameTile.TILE_WIDTH / 2) - region.getRegionWidth() / 2;
int y = (i * GameTile.TILE_HEIGHT / 2) + (j * GameTile.TILE_HEIGHT / 2) - region.getRegionHeight() / 2;
if (canDraw(x, y, GameTile.TILE_WIDTH, GameTile.TILE_HEIGHT)) {
batch.draw(region, x, y);
}
}
}
}
Actual tile before doing anything to it;
Actual:
Desired:
Converting Cartesian coordinates to isometric is (sort of) done like this:
float isometricX = cartesianX - cartesianY;
float isometricY = (cartesianX + cartesianY) * 0.5f;
The formula needs to be scaled by the height-to-width ratio of the tiles as well and I think that is where it's going wrong in your code.
Given an unprojected worldMousePosition you can get the coordinates and tile coordinates like this:
float r = (float) TILE_HEIGHT / (float) TILE_WIDTH;
float mapx = (worldMousePosition.x / TILE_HEIGHT + worldMousePosition.y / (TILE_HEIGHT * r)) * r;
float mapy = (worldMousePosition.y / (TILE_HEIGHT * r) - (worldMousePosition.x / TILE_HEIGHT)) * r;
worldPosition = new Vector2(mapx - 0.5f, mapy + 0.5f); // -.5/+.5 because the drawing isn't aligned to the tile, it's aligned to the image
int tileX = (int) worldPosition.x;
int tileY = (int) worldPosition.y;
Full source code for the example above:
import com.badlogic.gdx.Game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.math.Vector3;
public class SandboxGame extends Game {
public static final int TILE_NONE = -1;
public static final int MAP_WIDTH = 20;
public static final int MAP_HEIGHT = 36;
public static final int TILE_WIDTH = 64;
public static final int TILE_HEIGHT = 48;
private SpriteBatch batch;
private OrthographicCamera camera;
private BitmapFont font;
private Vector3 unprojectVector = new Vector3();
private Vector2 worldMousePosition = new Vector2();
private Vector2 worldPosition = new Vector2();
private Texture[] textures;
private int[][] tiles = new int[MAP_WIDTH][MAP_HEIGHT];
#Override
public void create() {
batch = new SpriteBatch();
camera = new OrthographicCamera(Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
font = new BitmapFont(Gdx.files.internal("default.fnt"), Gdx.files.internal("default.png"), false);
textures = new Texture[] {
new Texture(Gdx.files.internal("tile.png"))
};
for(int x = 0; x < MAP_WIDTH; ++x) {
for(int y = 0; y < MAP_HEIGHT; ++y) {
int rnd = MathUtils.random(10);
if (rnd < 1)
tiles[x][y] = TILE_NONE;
else
tiles[x][y] = 0;
}
}
}
#Override
public void render() {
Gdx.gl.glClearColor(0, 0, 0, 0);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
float scrollSpeed = 64;
float zoomSpeed = 2;
float delta = Gdx.graphics.getDeltaTime();
if (Gdx.input.isKeyPressed(Input.Keys.A))
camera.position.x -= delta * scrollSpeed;
if (Gdx.input.isKeyPressed(Input.Keys.D))
camera.position.x += delta * scrollSpeed;
if (Gdx.input.isKeyPressed(Input.Keys.W))
camera.position.y += delta * scrollSpeed;
if (Gdx.input.isKeyPressed(Input.Keys.S))
camera.position.y -= delta * scrollSpeed;
if (Gdx.input.isKeyPressed(Input.Keys.Q))
camera.zoom = Math.min(camera.zoom + zoomSpeed * delta, 8.0f);
if (Gdx.input.isKeyPressed(Input.Keys.E))
camera.zoom = Math.max(camera.zoom - zoomSpeed * delta, 0.5f);
camera.update();
int mx = Gdx.input.getX();
int my = Gdx.input.getY();
camera.unproject(unprojectVector.set(mx, my, 0.0f));
worldMousePosition.set(unprojectVector.x, unprojectVector.y);
float r = (float) TILE_HEIGHT / (float) TILE_WIDTH;
float mapx = (worldMousePosition.x / TILE_HEIGHT + worldMousePosition.y / (TILE_HEIGHT * r)) * r;
float mapy = (worldMousePosition.y / (TILE_HEIGHT * r) - (worldMousePosition.x / TILE_HEIGHT)) * r;
worldPosition = new Vector2(mapx - 0.5f, mapy + 0.5f); // -.5/+.5 because the drawing isn't aligned to the tile, it's aligned to the image
int tileX = (int) worldPosition.x;
int tileY = (int) worldPosition.y;
batch.setProjectionMatrix(camera.combined);
batch.begin();
for (int col = MAP_WIDTH - 1; col >= 0; --col) {
for (int row = MAP_HEIGHT - 1; row >= 0; --row) {
if (tiles[col][row] != TILE_NONE) {
Texture texture = textures[tiles[col][row]];
int x = (col * TILE_WIDTH / 2) - (row * TILE_WIDTH / 2);
int y = (col * TILE_HEIGHT / 2) + (row * TILE_HEIGHT / 2);
batch.setColor(col == tileX && row == tileY ? Color.GRAY : Color.WHITE);
batch.draw(texture, x, y);
}
}
}
if (Gdx.input.isKeyPressed(Input.Keys.SPACE)) {
for (int col = MAP_WIDTH - 1; col >= 0; --col) {
for (int row = MAP_HEIGHT - 1; row >= 0; --row) {
int x = (col * TILE_WIDTH / 2) - (row * TILE_WIDTH / 2);
int y = (col * TILE_HEIGHT / 2) + (row * TILE_HEIGHT / 2);
font.draw(batch, String.format("(%d, %d)", col, row), x, y);
}
}
}
String str = String.format("World position (%.2f, %.2f), Tile (%d, %d)", worldPosition.x, worldPosition.y, (int)worldPosition.x, (int)worldPosition.y);
font.draw(batch, str, worldMousePosition.x, worldMousePosition.y);
batch.end();
}
}
I cant respond to bornander's post, but my tweak would be at
int tileX = (int) Math.Floor(worldPosition.x);
int tileY = (int) Math.Floor(worldPosition.y);
Where simple (int) cast will provide wrong position around 0 with negative values, if there are tiles, while using Math.Floor will work as intended.
EDIT: NEW CODE AND OUTPUT AFTER DASHED LINE
This is for a school assignment but I just can't find anything that will rid me of this problem. What the program is suppose to do is take a GeneralPath object and create a Shape3D object with it using a class called RotatedShape. It has a constructor (the only one needed for the project) that takes the GeneralPath as a parameter. With this, the path is meant to be rotated around an axis to display a solid shape, similar to a Torus. The code is based off of code from the textbook I'm using, as most projects in the class usually are.
The exception happens here: qa.setCoordinates(i * m, ptsList); which is in the RotatedShape class, near the bottom. I know the problem is coming from using ptsList as a parameter because earlier I was having the same problem with that Point3d array when initializing it, hence why its size is currently set to 100 as a placeholder.
When run, the GUI displays nothing and I get this in my console output window:
x = 0.1 y = -0.5
Exception in thread "Thread-2" java.lang.NullPointerException
at javax.media.j3d.GeometryArrayRetained.setCoordinates(GeometryArrayRetained.java:3849)
at javax.media.j3d.GeometryArrayRetained.setCoordinates(GeometryArrayRetained.java:3849)
at javax.media.j3d.GeometryArray.setCoordinates(GeometryArray.java:1469)
at RotatedShape.<init>(Project8.java:202)
at Project8.createSceneGraph(Project8.java:58)
at Project8.init(Project8.java:27)
at com.sun.j3d.utils.applet.MainFrame.run(MainFrame.java:267)
at java.lang.Thread.run(Thread.java:724)
BUILD SUCCESSFUL (total time: 8 seconds)
I can clearly see where the error is occurring and I have an idea as to what the problem is, but I have no idea how to go about fixing it. It's probably something stupid about initializing the array or something, but I can't seem to find anything that helps.
Below is the code that is all in one file, as the professor wants it.
import javax.vecmath.*;
import java.awt.*;
import java.awt.event.*;
import javax.media.j3d.*;
import com.sun.j3d.utils.universe.*;
import com.sun.j3d.utils.geometry.*;
import java.applet.*;
import com.sun.j3d.utils.applet.MainFrame;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.util.Vector;
public class Project8 extends Applet {
public static void main(String[] args) {
new MainFrame(new Project8(), 640, 480);
}
public void init() {
// create canvas
GraphicsConfiguration gc
= SimpleUniverse.getPreferredConfiguration();
Canvas3D cv = new Canvas3D(gc);
setLayout(new BorderLayout());
add(cv, BorderLayout.CENTER);
BranchGroup bg = createSceneGraph();
bg.compile();
SimpleUniverse su = new SimpleUniverse(cv);
su.getViewingPlatform().setNominalViewingTransform();
su.addBranchGraph(bg);
}
private BranchGroup createSceneGraph() {
BranchGroup root = new BranchGroup();
TransformGroup spin = new TransformGroup();
spin.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
root.addChild(spin);
Transform3D tr = new Transform3D();
tr.setScale(0.8);
tr.setRotation(new AxisAngle4d(1, 0, 0, Math.PI / 6));
TransformGroup tg = new TransformGroup(tr);
spin.addChild(tg);
GeneralPath path = new GeneralPath();
path.moveTo(0.1f, -0.5f);
path.quadTo(0.5f, 1, 0.8f, 0.5f);
path.lineTo(1, 0.2f);
path.closePath();
Shape3D shape = new RotatedShape(path);
Appearance ap = new Appearance();
ap.setMaterial(new Material());
shape.setAppearance(ap);
tg.addChild(shape);
Alpha alpha = new Alpha(-1, 8000);
RotationInterpolator rotator = new RotationInterpolator(alpha, spin);
BoundingSphere bounds = new BoundingSphere();
rotator.setSchedulingBounds(bounds);
spin.addChild(rotator);
// background and lights
Background background = new Background(1.0f, 1.0f, 1.0f);
background.setApplicationBounds(bounds);
root.addChild(background);
AmbientLight light = new AmbientLight(true, new Color3f(Color.blue));
light.setInfluencingBounds(bounds);
root.addChild(light);
PointLight ptlight = new PointLight(new Color3f(Color.white),
new Point3f(3f, 3f, 3f), new Point3f(1f, 0f, 0f));
ptlight.setInfluencingBounds(bounds);
root.addChild(ptlight);
return root;
}
}
class RotatedShape extends Shape3D {
public RotatedShape(GeneralPath path) {
int depth = 100;
PathIterator iter = path.getPathIterator(new AffineTransform());
Point3d[] ptsList = new Point3d[depth];
float[] seg = new float[6];
float x = 0, y = 0;
float x0 = 0, y0 = 0;
int pos = 0;
while (!iter.isDone()) {
int segType = iter.currentSegment(seg);
switch (segType) {
case PathIterator.SEG_MOVETO:
x = x0 = seg[0];
y = y0 = seg[1];
System.out.println("x = " + x + " y = " + y);
ptsList[pos] = new Point3d(x, y, 0);
pos++;
break;
case PathIterator.SEG_LINETO:
x = seg[0];
y = seg[1];
ptsList[pos] = new Point3d(x, y, 0);
pos++;
break;
case PathIterator.SEG_QUADTO:
for (int i = 1; i < 10; i++) {
float t = (float) i / 10f;
float xi = (1 - t) * (1 - t) * x + 2 * t * (1 - t) * seg[0] + t * t * seg[2];
float yi = (1 - t) * (1 - t) * y + 2 * t * (1 - t) * seg[1] + t * t * seg[3];
ptsList[pos] = new Point3d(xi, yi, 0);
pos++;
}
x = seg[2];
y = seg[3];
ptsList[pos] = new Point3d(x, y, 0);
pos++;
break;
case PathIterator.SEG_CUBICTO:
for (int i = 1; i < 20; i++) {
float t = (float) i / 20f;
float xi = (1 - t) * (1 - t) * (1 - t) * x + 3 * t * (1 - t) * (1 - t) * seg[0]
+ 3 * t * t * (1 - t) * seg[2] + t * t * t * seg[4];
float yi = (1 - t) * (1 - t) * (1 - t) * y + 3 * t * (1 - t) * (1 - t) * seg[1]
+ 3 * t * t * (1 - t) * seg[3] + t * t * t * seg[5];
ptsList[pos] = new Point3d(xi, yi, 0);
pos++;
}
x = seg[2];
y = seg[3];
ptsList[pos] = new Point3d(x, y, 0);
pos++;
break;
case PathIterator.SEG_CLOSE:
x = x0;
y = y0;
ptsList[pos] = new Point3d(x, y, 0);
pos++;
break;
}
iter.next();
}
int m = 20;
//int n = 40;
int n = ptsList.length;
Transform3D rot2 = new Transform3D();
rot2.rotY(2.0 * Math.PI / n);
IndexedQuadArray qa = new IndexedQuadArray(m * n,
IndexedQuadArray.COORDINATES, 4 * m * n);
int quadIndex = 0;
for (int i = 0; i < n; i++) {
qa.setCoordinates(i * m, ptsList);
for (int j = 0; j < m; j++) {
rot2.transform(ptsList[j]);
int[] quadCoords = {i * m + j, ((i + 1) % n) * m + j,
((i + 1) % n) * m + ((j + 1) % m), i * m + ((j + 1) % m)};
qa.setCoordinateIndices(quadIndex, quadCoords);
quadIndex += 4;
}
}
GeometryInfo gi = new GeometryInfo(qa);
NormalGenerator ng = new NormalGenerator();
ng.generateNormals(gi);
this.setGeometry(gi.getGeometryArray());
}
}
import javax.vecmath.*;
import java.awt.*;
import java.awt.event.*;
import javax.media.j3d.*;
import com.sun.j3d.utils.universe.*;
import com.sun.j3d.utils.geometry.*;
import java.applet.*;
import com.sun.j3d.utils.applet.MainFrame;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.util.Vector;
public class Project8 extends Applet {
public static void main(String[] args) {
new MainFrame(new Project8(), 640, 480);
}
public void init() {
// create canvas
GraphicsConfiguration gc
= SimpleUniverse.getPreferredConfiguration();
Canvas3D cv = new Canvas3D(gc);
setLayout(new BorderLayout());
add(cv, BorderLayout.CENTER);
BranchGroup bg = createSceneGraph();
bg.compile();
SimpleUniverse su = new SimpleUniverse(cv);
su.getViewingPlatform().setNominalViewingTransform();
su.addBranchGraph(bg);
}
private BranchGroup createSceneGraph() {
BranchGroup root = new BranchGroup();
TransformGroup spin = new TransformGroup();
spin.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
root.addChild(spin);
Transform3D tr = new Transform3D();
tr.setScale(0.8);
tr.setRotation(new AxisAngle4d(1, 0, 0, Math.PI / 6));
TransformGroup tg = new TransformGroup(tr);
spin.addChild(tg);
GeneralPath path = new GeneralPath();
path.moveTo(0.1f, -0.5f);
path.quadTo(0.5f, 1, 0.8f, 0.5f);
path.lineTo(1, 0.2f);
path.closePath();
Shape3D shape = new RotatedShape(path);
Appearance ap = new Appearance();
ap.setMaterial(new Material());
shape.setAppearance(ap);
tg.addChild(shape);
Alpha alpha = new Alpha(-1, 8000);
RotationInterpolator rotator = new RotationInterpolator(alpha, spin);
BoundingSphere bounds = new BoundingSphere();
rotator.setSchedulingBounds(bounds);
spin.addChild(rotator);
// background and lights
Background background = new Background(1.0f, 1.0f, 1.0f);
background.setApplicationBounds(bounds);
root.addChild(background);
AmbientLight light = new AmbientLight(true, new Color3f(Color.blue));
light.setInfluencingBounds(bounds);
root.addChild(light);
PointLight ptlight = new PointLight(new Color3f(Color.white),
new Point3f(3f, 3f, 3f), new Point3f(1f, 0f, 0f));
ptlight.setInfluencingBounds(bounds);
root.addChild(ptlight);
return root;
}
}
class RotatedShape extends Shape3D {
public RotatedShape(GeneralPath path) {
int depth = 100;
PathIterator iter = path.getPathIterator(new AffineTransform());
Vector ptsVector = new Vector();
float[] seg = new float[6];
float x = 0, y = 0;
float x0 = 0, y0 = 0;
int pos = 0;
while (!iter.isDone()) {
int segType = iter.currentSegment(seg);
switch (segType) {
case PathIterator.SEG_MOVETO:
x = x0 = seg[0];
y = y0 = seg[1];
ptsVector.add(new Point3d(x, y, 0));
pos++;
break;
case PathIterator.SEG_LINETO:
x = seg[0];
y = seg[1];
ptsVector.add(new Point3d(x, y, 0));
pos++;
break;
case PathIterator.SEG_QUADTO:
for (int i = 1; i < 10; i++) {
float t = (float) i / 10f;
float xi = (1 - t) * (1 - t) * x + 2 * t * (1 - t) * seg[0] + t * t * seg[2];
float yi = (1 - t) * (1 - t) * y + 2 * t * (1 - t) * seg[1] + t * t * seg[3];
ptsVector.add(new Point3d(xi, yi, 0));
pos++;
}
x = seg[2];
y = seg[3];
ptsVector.add(new Point3d(x, y, 0));
pos++;
break;
case PathIterator.SEG_CUBICTO:
for (int i = 1; i < 20; i++) {
float t = (float) i / 20f;
float xi = (1 - t) * (1 - t) * (1 - t) * x + 3 * t * (1 - t) * (1 - t) * seg[0]
+ 3 * t * t * (1 - t) * seg[2] + t * t * t * seg[4];
float yi = (1 - t) * (1 - t) * (1 - t) * y + 3 * t * (1 - t) * (1 - t) * seg[1]
+ 3 * t * t * (1 - t) * seg[3] + t * t * t * seg[5];
ptsVector.add(new Point3d(xi, yi, 0));
pos++;
}
x = seg[2];
y = seg[3];
ptsVector.add(new Point3d(x, y, 0));
pos++;
break;
case PathIterator.SEG_CLOSE:
x = x0;
y = y0;
ptsVector.add(new Point3d(x, y, 0));
pos++;
break;
}
iter.next();
}
for (int i = 0; i < ptsVector.size(); i++)
System.out.println("Vector = " + ptsVector.elementAt(i) + " at " + i);
System.out.println("pos = " + pos);
Point3d[] ptsArray = null;
for (int i = 0; i < ptsVector.size(); i++) {
ptsArray[i] = (Point3d) ptsVector.elementAt(i);
}
// Point3d[] ptsArray = ptsVector.toArray(new Point3d(ptsVector.size()));
int m = 20;
//int n = 40;
int n = ptsArray.length;
Transform3D rot2 = new Transform3D();
rot2.rotY(2.0 * Math.PI / n);
IndexedQuadArray qa = new IndexedQuadArray(m * n,
IndexedQuadArray.COORDINATES, 4 * m * n);
int quadIndex = 0;
for (int i = 0; i < n; i++) {
System.out.println("i = " + i);
qa.setCoordinates(i * m, ptsArray);
for (int j = 0; j < m; j++) {
rot2.transform(ptsArray[j]);
int[] quadCoords = {i * m + j, ((i + 1) % n) * m + j,
((i + 1) % n) * m + ((j + 1) % m), i * m + ((j + 1) % m)};
qa.setCoordinateIndices(quadIndex, quadCoords);
quadIndex += 4;
}
}
GeometryInfo gi = new GeometryInfo(qa);
NormalGenerator ng = new NormalGenerator();
ng.generateNormals(gi);
this.setGeometry(gi.getGeometryArray());
}
}
Output:
run:
Vector = (0.10000000149011612, -0.5, 0.0) at 0
Vector = (0.17899999022483826, -0.2199999839067459, 0.0) at 1
Vector = (0.25600001215934753, 0.020000001415610313, 0.0) at 2
Vector = (0.3310000002384186, 0.2200000286102295, 0.0) at 3
Vector = (0.40400004386901855, 0.3800000250339508, 0.0) at 4
Vector = (0.4750000238418579, 0.5, 0.0) at 5
Exception in thread "Thread-2" java.lang.NullPointerException
Vector = (0.5440000295639038, 0.5800000429153442, 0.0) at 6
at RotatedShape.<init>(Project8.java:197)
Vector = (0.6110000014305115, 0.6200000047683716, 0.0) at 7
Vector = (0.6759999990463257, 0.6200000047683716, 0.0) at 8
Vector = (0.7389999628067017, 0.5800000429153442, 0.0) at 9
Vector = (0.800000011920929, 0.5, 0.0) at 10
Vector = (1.0, 0.20000000298023224, 0.0) at 11
Vector = (0.10000000149011612, -0.5, 0.0) at 12
pos = 13
at Project8.createSceneGraph(Project8.java:58)
at Project8.init(Project8.java:27)
at com.sun.j3d.utils.applet.MainFrame.run(MainFrame.java:267)
at com.sun.j3d.utils.applet.MainFrame.run(MainFrame.java:267)
at java.lang.Thread.run(Thread.java:724)
I'm writing a Raytracer in Java, I've gotten to the point where I can create objects, rays, test for intersections and then colour pixels. I've also got some basic anti aliasing done. My problem is that if a create a sphere, which should be in the centre of the world (i.e 0.0, 0.0, 0.0) and then draw the image, I end up with a picture like this.
When the red circle should be in the middle of the image.
Main method
public static void main(String[] args) {
System.out.println("Rendering...");
long start = System.nanoTime();
// Setting up the size of the image to be rendered
world = new World(1920, 1080, 1.0);
image = new Image("image.png");
sampler = new SimpleSampler(4);
projector = new OrthographicProjector();
// Main loop of program, goes through each pixel in image and assigns a colour value
for (int y = 0; y < world.viewPlane.height; y++) {
for (int x = 0; x < world.viewPlane.width; x++) {
// Render pixel colour
trace(x, y);
}
}
image.saveImage("PNG");
long end = System.nanoTime();
System.out.print("Loop Time = " + ((end - start)/1000000000.0f));
}
Trace method
public static void trace(int x, int y) {
Colour colour = new Colour();
//int colour = RayTracer.world.backgroundColour.toInteger();
for (int col = 0; col < sampler.samples; col++) {
for (int row = 0; row < sampler.samples; row++) {
Point2D point = sampler.sample(row, col, x, y);
Ray ray = projector.createRay(point);
double min = Double.MAX_VALUE;
Colour tempColour = new Colour();
for (int i = 0; i < world.worldObjects.size(); i++) {
double temp = world.worldObjects.get(i).intersect(ray);
if (temp != 0 && temp < min) {
min = temp;
tempColour = world.worldObjects.get(i).colour;
}
}
colour.add(tempColour);
}
}
colour.divide(sampler.samples*sampler.samples);
image.buffer.setRGB(x, y, colour.toInteger());
}
World.java
public class World {
public ViewPlane viewPlane;
public ArrayList<Renderable> worldObjects;
public Colour backgroundColour;
public World(int width, int height, double size) {
viewPlane = new ViewPlane(width, height, size);
backgroundColour = new Colour(0.0f, 0.0f, 0.0f);
worldObjects = new ArrayList<Renderable>();
worldObjects.add(new Sphere(new Point3D(0.0, 0.0, 0.0), 50, new Colour(1.0f, 0.0f, 0.0f)));
//worldObjects.add(new Sphere(new Point3D(-150.0, 0.0, 0.0), 50, new Colour(1.0f, 0.0f, 0.0f)));
//worldObjects.add(new Sphere(new Point3D(0.0, -540.0, 0.0), 50, new Colour(0.0f, 1.0f, 0.0f)));
}
}
SimpleSampler.java
public class SimpleSampler extends Sampler {
public SimpleSampler(int samples) {
this.samples = samples;
}
public Point2D sample(int row, int col, int x, int y) {
Point2D point = new Point2D(
x - RayTracer.world.viewPlane.width / 2 + (col + 0.5) / samples,
y - RayTracer.world.viewPlane.width / 2 + (row + 0.5) / samples);
return point;
}
}
OrthographicProjector.java
public class OrthographicProjector extends Projector{
public Ray createRay(Point2D point) {
Ray ray = new Ray();
ray.origin = new Point3D(
RayTracer.world.viewPlane.size * point.x,
RayTracer.world.viewPlane.size * point.y,
100);
ray.direction = new Vector3D(0.0, 0.0, -1.0);
return ray;
}
}
I have a feeling that somewhere along the way I've mixed an x with a y and this has rotated the image, but I haven't been able to track down the problem. If you would like to see any more of my code I would be happy to show it.
In SimpleSampler.java:
Point2D point = new Point2D(
x - RayTracer.world.viewPlane.width / 2 + (col + 0.5) / samples,
y - RayTracer.world.viewPlane.width / 2 + (row + 0.5) / samples);
You use width for both coordinates. Maybe you should use width and height.
I'm not good in math.
I have 2 points, A(x1, y1) and B(x2, y2) in 2D.
I need to create a virtual path from point A to B curved at R(radius), and then return an array of points which are describing this curved path, not all maybe every D(distance) from each other.
In Java I need a method like this:
private ArrayList<PointF> generateCurve(PointF pFrom,PointF pTo,float pRadius,float pMinDistance){
ArrayList<PointF> pOutPut = new ArrayList<PointF>();
// ...generate result to pOutPut
return pOutPut;
}
How to do this ?
I didn't gave up and I've been working on it for a few more hours. And here is the result:
I created a method where you can specify if you want the shortest of the longest arc between the points.
Here are some calls to it, with the produced output:
generateCurve(pFrom, pTo, 100f, 7f, false, false);
generateCurve(pFrom, pTo, 100f, 7f, true, false);
generateCurve(pFrom, pTo, 100f, 7f, false, true);
generateCurve(pFrom, pTo, 100f, 7f, true, true);
As you can see, it is working like a charm. Here is the code:
package curve;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.imageio.ImageIO;
/**
*
* #author martijn
*/
public class Main
{
/**
* #param args the command line arguments
*/
public static void main(String[] args) throws IOException
{
PointF pFrom = new PointF(-10f, 30.0f);
PointF pTo = new PointF(-100f, 0.0f);
List<PointF> points = generateCurve(pFrom, pTo, 100f, 7f, true, true);
System.out.println(points);
// Calculate the bounds of the curve
Rectangle2D.Float bounds = new Rectangle2D.Float(points.get(0).x, points.get(0).y, 0, 0);
for (int i = 1; i < points.size(); ++i) {
bounds.add(points.get(i).x, points.get(i).y);
}
bounds.add(pFrom.x, pFrom.y);
bounds.add(pTo.x, pTo.y);
BufferedImage img = new BufferedImage((int) (bounds.width - bounds.x + 50), (int) (bounds.height - bounds.y + 50), BufferedImage.TYPE_4BYTE_ABGR_PRE);
Graphics2D g = img.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.translate(25.0f - bounds.getX(), 25.0f - bounds.getY());
g.setStroke(new BasicStroke(1.0f));
g.setColor(Color.DARK_GRAY);
g.drawLine(-1000, 0, 1000, 0);
g.drawLine(0, -1000, 0, 1000);
g.setColor(Color.RED);
for (int i = 0; i < points.size(); ++i) {
if (i > 0) {
Line2D.Float f = new Line2D.Float(points.get(i - 1).x, points.get(i - 1).y, points.get(i).x, points.get(i).y);
System.out.println("Dist : " + f.getP1().distance(f.getP2()));
// g.draw(f);
}
g.fill(new Ellipse2D.Float(points.get(i).x - 0.8f, points.get(i).y - 0.8f, 1.6f, 1.6f));
}
g.setColor(Color.BLUE);
g.fill(new Ellipse2D.Float(pFrom.x - 1, pFrom.y - 1, 3, 3));
g.fill(new Ellipse2D.Float(pTo.x - 1, pTo.y - 1, 3, 3));
g.dispose();
ImageIO.write(img, "PNG", new File("result.png"));
}
static class PointF
{
public float x, y;
public PointF(float x, float y)
{
this.x = x;
this.y = y;
}
#Override
public String toString()
{
return "(" + x + "," + y + ")";
}
}
private static List<PointF> generateCurve(PointF pFrom, PointF pTo, float pRadius, float pMinDistance, boolean shortest, boolean side)
{
List<PointF> pOutPut = new ArrayList<PointF>();
// Calculate the middle of the two given points.
PointF mPoint = new PointF(pFrom.x + pTo.x, pFrom.y + pTo.y);
mPoint.x /= 2.0f;
mPoint.y /= 2.0f;
System.out.println("Middle Between From and To = " + mPoint);
// Calculate the distance between the two points
float xDiff = pTo.x - pFrom.x;
float yDiff = pTo.y - pFrom.y;
float distance = (float) Math.sqrt(xDiff * xDiff + yDiff * yDiff);
System.out.println("Distance between From and To = " + distance);
if (pRadius * 2.0f < distance) {
throw new IllegalArgumentException("The radius is too small! The given points wont fall on the circle.");
}
// Calculate the middle of the expected curve.
float factor = (float) Math.sqrt((pRadius * pRadius) / ((pTo.x - pFrom.x) * (pTo.x - pFrom.x) + (pTo.y - pFrom.y) * (pTo.y - pFrom.y)) - 0.25f);
PointF circleMiddlePoint = new PointF(0, 0);
if (side) {
circleMiddlePoint.x = 0.5f * (pFrom.x + pTo.x) + factor * (pTo.y - pFrom.y);
circleMiddlePoint.y = 0.5f * (pFrom.y + pTo.y) + factor * (pFrom.x - pTo.x);
} else {
circleMiddlePoint.x = 0.5f * (pFrom.x + pTo.x) - factor * (pTo.y - pFrom.y);
circleMiddlePoint.y = 0.5f * (pFrom.y + pTo.y) - factor * (pFrom.x - pTo.x);
}
System.out.println("Middle = " + circleMiddlePoint);
// Calculate the two reference angles
float angle1 = (float) Math.atan2(pFrom.y - circleMiddlePoint.y, pFrom.x - circleMiddlePoint.x);
float angle2 = (float) Math.atan2(pTo.y - circleMiddlePoint.y, pTo.x - circleMiddlePoint.x);
// Calculate the step.
float step = pMinDistance / pRadius;
System.out.println("Step = " + step);
// Swap them if needed
if (angle1 > angle2) {
float temp = angle1;
angle1 = angle2;
angle2 = temp;
}
boolean flipped = false;
if (!shortest) {
if (angle2 - angle1 < Math.PI) {
float temp = angle1;
angle1 = angle2;
angle2 = temp;
angle2 += Math.PI * 2.0f;
flipped = true;
}
}
for (float f = angle1; f < angle2; f += step) {
PointF p = new PointF((float) Math.cos(f) * pRadius + circleMiddlePoint.x, (float) Math.sin(f) * pRadius + circleMiddlePoint.y);
pOutPut.add(p);
}
if (flipped ^ side) {
pOutPut.add(pFrom);
} else {
pOutPut.add(pTo);
}
return pOutPut;
}
}
Enjoy!
PS: I created two questions on Mathematics to solve your question:
Analytic Geometry: Point coordinates, same distance from two points.
Trigonometry: Solve (1−cosα)2+sin2α=d2 for α
This works:
private static double GetAngle(Point2D x, Point2D o, double R){
double cosa = (x.getX()-o.getX())/R;
double sina = (x.getY()-o.getY())/R;
double angle = Math.acos(cosa);
return Math.sin(angle)*sina >= 0 ? angle : 2*Math.PI - angle;
}
private static ArrayList<Point2D> generateCurve(Point2D pFrom,Point2D pTo,float pRadius,float pMinDistance){
ArrayList<Point2D> pOutPut = new ArrayList<Point2D>();
double dist = pFrom.distance(pTo);
double h = Math.sqrt(pRadius * pRadius - (dist * dist / 4.0));
double angleStep = pMinDistance/pRadius;
if(2*pRadius <= dist)
throw new Error("Radius is too small");
//find center
double x1 = pFrom.getX(), x2 = pFrom.getY();
double y1 = pTo.getX(), y2 = pTo.getY();
double m1 = (x1+y1)/2, m2 = (x2+y2)/2;
double u1 = - (y2-x2)/dist, u2 = (y1-x1)/dist;
double o1 = m1 + h * u1, o2 = m2 + h * u2;
Point2D o = new Point2D.Double(o1, o2);
double startAngle = GetAngle(pFrom, o, pRadius);
double endAngle = GetAngle(pTo, o, pRadius);
if(endAngle < startAngle)
endAngle += 2 * Math.PI;
for(double a = startAngle; a < endAngle; a+=angleStep){
pOutPut.add(new Point2D.Double(o1+pRadius*Math.cos(a), o2+pRadius*Math.sin(a)));
}
pOutPut.add(pTo);
return pOutPut;
}
Here is what I get when I call it like this: generateCurve(new Point2D.Double(10,10), new Point2D.Double(400, 400), 300, 15)
I have a standalone Java application below that is:
Generating a random line
Applied to a 2D grid where each cell value is the distance along the line perpindicular to the line
Finds the rise/run and attempts to calculate the original linear equation from the grid
Applies new line to another grid and prints out the greatest difference compared to the first grid
I expected the two grids to have identical values. The gradient lines may be different since the lines can extend outside the area of the grid, but should be similar and in two cases identical.
So is the problem a poor understanding of math, a bug in my code or a misunderstanding of floating point values?
import java.awt.geom.Point2D;
import java.awt.geom.Line2D;
import java.util.Iterator;
import java.util.ArrayList;
public final class TestGradientLine {
private static int SIZE = 3;
public TestGradientLine() {
super();
}
//y = mx + b
//b = y - mx
//m is rise / run = gradient
//width and height of bounding box
//for a box 10x10 then width and height are 9,9
public static Line2D getGradientLine(double run, double rise, double width, double height, double x, double y) {
if (run == 0 && rise == 0) {
return new Line2D.Double(x, y, x + width, y + height);
}
//calculate hypotenuse
//check for a vertical line
if (run == 0) {
return new Line2D.Double(x, y, x, y + height);
}
//check for a horizontal line
if (rise == 0) {
return new Line2D.Double(x, y, x + width, y);
}
//calculate gradient
double m = rise / run;
Point2D start;
Point2D opposite;
if (m < 0) {
//lower left
start = new Point2D.Double(x, y + height);
opposite = new Point2D.Double(x + width, y);
} else {
//upper left
start = new Point2D.Double(x, y);
opposite = new Point2D.Double(x + width, y + height);
}
double b = start.getY() - (m * start.getX());
//now calculate another point along the slope
Point2D next = null;
if (m > 0) {
next = new Point2D.Double(start.getX() + Math.abs(run), start.getY() + Math.abs(rise));
} else {
if (rise < 0) {
next = new Point2D.Double(start.getX() + run, start.getY() + rise);
} else {
next = new Point2D.Double(start.getX() - run, start.getY() - rise);
}
}
final double actualWidth = width;
final double actualHeight = height;
final double a = Math.sqrt((actualWidth * actualWidth) + (actualHeight * actualHeight));
extendLine(start, next, a);
Line2D gradientLine = new Line2D.Double(start, next);
return gradientLine;
}
public static void extendLine(Point2D p0, Point2D p1, double toLength) {
final double oldLength = p0.distance(p1);
final double lengthFraction =
oldLength != 0.0 ? toLength / oldLength : 0.0;
p1.setLocation(p0.getX() + (p1.getX() - p0.getX()) * lengthFraction,
p0.getY() + (p1.getY() - p0.getY()) * lengthFraction);
}
public static Line2D generateRandomGradientLine(int width, int height) {
//so true means lower and false means upper
final boolean isLower = Math.random() > .5;
final Point2D start = new Point2D.Float(0, 0);
if (isLower) {
//change origin for lower left corner
start.setLocation(start.getX(), height - 1);
}
//radius of our circle
double radius = Math.sqrt(width * width + height * height);
//now we want a random theta
//x = r * cos(theta)
//y = r * sin(theta)
double theta = 0.0;
if (isLower) {
theta = Math.random() * (Math.PI / 2);
} else {
theta = Math.random() * (Math.PI / 2) + (Math.PI / 2);
}
int endX = (int)Math.round(radius * Math.sin(theta));
int endY = (int)Math.round(radius * Math.cos(theta)) * -1;
if (isLower) {
endY = endY + (height - 1);
}
final Point2D end = new Point2D.Float(endX, endY);
extendLine(start, end, radius);
return new Line2D.Float(start, end);
}
public static Point2D getNearestPointOnLine(Point2D end, Line2D line) {
final Point2D point = line.getP1();
final Point2D start = line.getP2();
double a = (end.getX() - point.getX()) * (start.getX() - point.getX()) + (end.getY() - point.getY()) * (start.getY() - point.getY());
double b = (end.getX() - start.getX()) * (point.getX() - start.getX()) + (end.getY() - start.getY()) * (point.getY() - start.getY());
final double x = point.getX() + ((start.getX() - point.getX()) * a)/(a + b);
final double y = point.getY() + ((start.getY() - point.getY()) * a)/(a + b);
final Point2D result = new Point2D.Double(x, y);
return result;
}
public static double length(double x0, double y0, double x1, double y1) {
final double dx = x1 - x0;
final double dy = y1 - y0;
return Math.sqrt(dx * dx + dy * dy);
}
public static void main(String[] args) {
final Line2D line = generateRandomGradientLine(SIZE, SIZE);
System.out.println("we're starting with line " + line.getP1() + " " + line.getP2());
double[][] region = new double[SIZE][SIZE];
//load up the region with data from our generated line
for (int x = 0; x < SIZE; x++) {
for (int y = 0; y < SIZE; y++) {
final Point2D point = new Point2D.Double(x, y);
final Point2D nearestPoint = getNearestPointOnLine(point, line);
if (nearestPoint == null) {
System.err.println("uh -oh!");
return;
}
final double distance = length(line.getP1().getX(),
line.getP1().getY(), nearestPoint.getX() + 1,
nearestPoint.getY() + 1);
region[x][y] = distance;
}
}
//now figure out what our line is from the region
double runTotal = 0;
double riseTotal = 0;
double runCount = 0;
double riseCount = 0;
for (int x = 0; x < SIZE; x++) {
for (int y = 0; y < SIZE; y++) {
if (x < SIZE - 1) {
runTotal += region[x + 1][y] - region[x][y];
runCount++;
}
if (y < SIZE - 1) {
riseTotal += region[x][y + 1] - region[x][y];
riseCount++;
}
}
}
double run = 0;
if (runCount > 0) {
run = runTotal / runCount;
}
double rise = 0;
if (riseCount > 0) {
rise = riseTotal / riseCount;
}
System.out.println("rise is " + rise + " run is " + run);
Line2D newLine = getGradientLine(run, rise, SIZE - 1, SIZE - 1, 0, 0);
System.out.println("ending with line " + newLine.getP1() + " " + newLine.getP2());
double worst = 0.0;
int worstX = 0;
int worstY = 0;
for (int x = 0; x < SIZE; x++) {
for (int y = 0; y < SIZE; y++) {
final Point2D point = new Point2D.Double(x, y);
final Point2D nearestPoint = getNearestPointOnLine(point, newLine);
if (nearestPoint == null) {
System.err.println("uh -oh!");
return;
}
final double distance = length(line.getP1().getX(),
line.getP1().getY(), nearestPoint.getX() + 1,
nearestPoint.getY() + 1);
final double diff = Math.abs(region[x][y] - distance);
if (diff > worst) {
worst = diff;
worstX = x;
worstY = y;
}
}
}
System.out.println("worst is " + worst + " x: " + worstX + " y: " + worstY);
}
}
I think I have fixed your program.
a) I took out the integer cast.
b) I removed all the 'x + 1' and 'x - 1' fudges you had used.
I think when dealing with floats and doubles, subtracting '1' from the end of a line is a No-No! What is 1 anyway? - it's ok to do this just before you plot it on the screen once it's an integer. But not while calculating! line length is a 'zero-based' quantity.
This version returns approx 4E-16 always.
import java.awt.geom.Point2D;
import java.awt.geom.Line2D;
import java.awt.geom.QuadCurve2D;
import java.util.Iterator;
import java.util.ArrayList;
public final class TestGradientLine {
private static int SIZE = 3;
public TestGradientLine() {
super();
}
//y = mx + b
//b = y - mx
//m is rise / run = gradient
//width and height of bounding box
//for a box 10x10 then width and height are 9,9
public static Line2D getGradientLine(double run, double rise, double width, double height, double x, double y) {
if (run == 0 && rise == 0) {
return new Line2D.Double(x, y, x + width, y + height);
}
//calculate hypotenuse
//check for a vertical line
if (run == 0) {
return new Line2D.Double(x, y, x, y + height);
}
//check for a horizontal line
if (rise == 0) {
return new Line2D.Double(x, y, x + width, y);
}
//calculate gradient
double m = rise / run;
Point2D start;
Point2D opposite;
if (m < 0) {
//lower left
start = new Point2D.Double(x, y + height);
opposite = new Point2D.Double(x + width, y);
} else {
//upper left
start = new Point2D.Double(x, y);
opposite = new Point2D.Double(x + width, y + height);
}
double b = start.getY() - (m * start.getX());
//now calculate another point along the slope
Point2D next = null;
if (m > 0) {
next = new Point2D.Double(start.getX() + Math.abs(run), start.getY() + Math.abs(rise));
} else {
if (rise < 0) {
next = new Point2D.Double(start.getX() + run, start.getY() + rise);
} else {
next = new Point2D.Double(start.getX() - run, start.getY() - rise);
}
}
final double actualWidth = width;
final double actualHeight = height;
final double a = Math.sqrt((actualWidth * actualWidth) + (actualHeight * actualHeight));
extendLine(start, next, a);
Line2D gradientLine = new Line2D.Double(start, next);
return gradientLine;
}
public static void extendLine(Point2D p0, Point2D p1, double toLength) {
final double oldLength = p0.distance(p1);
final double lengthFraction =
oldLength != 0.0 ? toLength / oldLength : 0.0;
p1.setLocation(p0.getX() + (p1.getX() - p0.getX()) * lengthFraction,
p0.getY() + (p1.getY() - p0.getY()) * lengthFraction);
}
public static Line2D generateRandomGradientLine(int width, int height) {
//so true means lower and false means upper
final boolean isLower = Math.random() > .5;
final Point2D start = new Point2D.Float(0, 0);
if (isLower) {
//change origin for lower left corner
start.setLocation(start.getX(), height );
}
//radius of our circle
double radius = Math.sqrt(width * width + height * height);
//now we want a random theta
//x = r * cos(theta)
//y = r * sin(theta)
double theta = 0.0;
if (isLower) {
theta = Math.random() * (Math.PI / 2);
} else {
theta = Math.random() * (Math.PI / 2) + (Math.PI / 2);
}
float endX = (float)(radius * Math.sin(theta));
float endY = (float)(radius * Math.cos(theta)) * -1;
if (isLower) {
endY = endY + (height );
}
final Point2D end = new Point2D.Float(endX, endY);
extendLine(start, end, radius);
return new Line2D.Float(start, end);
}
public static Point2D getNearestPointOnLine(Point2D end, Line2D line) {
final Point2D point = line.getP1();
final Point2D start = line.getP2();
double a = (end.getX() - point.getX()) * (start.getX() - point.getX()) + (end.getY() - point.getY()) * (start.getY() - point.getY());
double b = (end.getX() - start.getX()) * (point.getX() - start.getX()) + (end.getY() - start.getY()) * (point.getY() - start.getY());
final double x = point.getX() + ((start.getX() - point.getX()) * a)/(a+b);
final double y = point.getY() + ((start.getY() - point.getY()) * a)/(a+b);
final Point2D result = new Point2D.Double(x, y);
return result;
}
public static double length(double x0, double y0, double x1, double y1) {
final double dx = x1 - x0;
final double dy = y1 - y0;
return Math.sqrt(dx * dx + dy * dy);
}
public static void main(String[] args) {
final Line2D line = generateRandomGradientLine(SIZE, SIZE);
System.out.println("we're starting with line " + line.getP1() + " " + line.getP2());
double[][] region = new double[SIZE][SIZE];
//load up the region with data from our generated line
for (int x = 0; x < SIZE; x++) {
for (int y = 0; y < SIZE; y++) {
final Point2D point = new Point2D.Double(x, y);
final Point2D nearestPoint = getNearestPointOnLine(point, line);
if (nearestPoint == null) {
System.err.println("uh -oh!");
return;
}
final double distance = length(line.getP1().getX(),
line.getP1().getY(), nearestPoint.getX() ,
nearestPoint.getY() );
region[x][y] = distance;
}
}
//now figure out what our line is from the region
double runTotal = 0;
double riseTotal = 0;
double runCount = 0;
double riseCount = 0;
for (int x = 0; x < SIZE; x++) {
for (int y = 0; y < SIZE; y++) {
if (x < SIZE - 1) {
runTotal += region[x + 1][y] - region[x][y];
runCount++;
}
if (y < SIZE - 1) {
riseTotal += region[x][y + 1] - region[x][y];
riseCount++;
}
}
}
double run = 0;
if (runCount > 0) {
run = runTotal / runCount;
}
double rise = 0;
if (riseCount > 0) {
rise = riseTotal / riseCount;
}
System.out.println("rise is " + rise + " run is " + run);
Line2D newLine = getGradientLine(run, rise, SIZE, SIZE , 0, 0);
System.out.println("ending with line " + newLine.getP1() + " " + newLine.getP2());
double worst = 0.0;
int worstX = 0;
int worstY = 0;
for (int x = 0; x < SIZE; x++) {
for (int y = 0; y < SIZE; y++) {
final Point2D point = new Point2D.Double(x, y);
final Point2D nearestPoint = getNearestPointOnLine(point, newLine);
if (nearestPoint == null) {
System.err.println("uh -oh!");
return;
}
final double distance = length(line.getP1().getX(),
line.getP1().getY(), nearestPoint.getX() ,
nearestPoint.getY() );
final double diff = Math.abs(region[x][y] - distance);
if (diff > worst) {
worst = diff;
worstX = x;
worstY = y;
}
}
}
System.out.println("worst is " + worst + " x: " + worstX + " y: " + worstY);
}
}
why do you multiply by -1 at the end of this line?
int endY = (int)Math.round(radius * Math.cos(theta)) * -1;
this means that endY is always negative except radius is below 0. (cosinus always returns positive value)
is this intended or am i getting something wrong?
regards
You probably misunderstand float and/or double. This is a common problem with any language that implements the ieee spec for floats and doubles, which Java, C, C++ and just about every other language does.
Essentially
double val = 0;
for(int i=0;i<10;i++) {
val+=0.1;
System.out.println(val);
}
results in
0.1
0.2
0.30000000000000004
0.4
0.5
0.6
0.7
0.7999999999999999
0.8999999999999999
0.9999999999999999
And sometimes even worse. Either use BigDecimal, which alleviates a lot of the problem, or use integers.