/*
* JaamSim Discrete Event Simulation
* Copyright (C) 2012 Ausenco Engineering Canada Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jaamsim.render;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import com.jaamsim.math.Mat4d;
import com.jaamsim.math.Plane;
import com.jaamsim.math.Ray;
import com.jaamsim.math.Transform;
import com.jaamsim.math.Vec2d;
import com.jaamsim.math.Vec3d;
import com.jaamsim.math.Vec4d;
import com.jaamsim.ui.LogBox;
/**
* A big pile of static methods that currently don't have a better place to live. All Rendering specific
* @author matt.chudleigh
*
*/
public class RenderUtils {
public static List<Vec4d> CIRCLE_POINTS;
public static List<Vec4d> RECT_POINTS;
public static List<Vec4d> TRIANGLE_POINTS;
static {
CIRCLE_POINTS = getCirclePoints(32);
// Scale the points down (as JaamSim uses a 1x1 box [-0.5, 0.5] not [-1, 1]
for (int i = 0; i < CIRCLE_POINTS.size(); ++i) {
CIRCLE_POINTS.get(i).scale3(0.5);
}
RECT_POINTS = new ArrayList<>();
RECT_POINTS.add(new Vec4d( 0.5, 0.5, 0, 1.0d));
RECT_POINTS.add(new Vec4d(-0.5, 0.5, 0, 1.0d));
RECT_POINTS.add(new Vec4d(-0.5, -0.5, 0, 1.0d));
RECT_POINTS.add(new Vec4d( 0.5, -0.5, 0, 1.0d));
TRIANGLE_POINTS = new ArrayList<>();
TRIANGLE_POINTS.add(new Vec4d( 0.5, -0.5, 0, 1.0d));
TRIANGLE_POINTS.add(new Vec4d( 0.5, 0.5, 0, 1.0d));
TRIANGLE_POINTS.add(new Vec4d(-0.5, 0.0, 0, 1.0d));
}
// Transform the list of points in place
public static void transformPointsLocal(Transform trans, List<Vec4d> points, int dummy) {
for (Vec4d p : points) {
trans.apply(p, p);
}
}
public static List<Vec4d> transformPoints(Mat4d mat, List<Vec4d> points, int dummy) {
List<Vec4d> ret = new ArrayList<>();
for (Vec4d p : points) {
Vec4d v = new Vec4d(0.0d, 0.0d, 0.0d, 1.0d);
v.mult4(mat, p);
ret.add(v);
}
return ret;
}
public static List<Vec3d> transformPointsWithTrans(Mat4d mat, List<Vec3d> points) {
List<Vec3d> ret = new ArrayList<>();
for (Vec3d p : points) {
Vec3d v = new Vec3d();
v.multAndTrans3(mat, p);
ret.add(v);
}
return ret;
}
static void putPointXY(FloatBuffer fb, Vec2d v) {
fb.put((float)v.x);
fb.put((float)v.y);
}
static void putPointXYZ(FloatBuffer fb, Vec3d v) {
fb.put((float)v.x);
fb.put((float)v.y);
fb.put((float)v.z);
}
static void putPointXYZW(FloatBuffer fb, Vec4d v) {
fb.put((float)v.x);
fb.put((float)v.y);
fb.put((float)v.z);
fb.put((float)v.w);
}
/**
* Returns a list of points for a circle in the XY plane at the origin
* @return
*/
public static ArrayList<Vec4d> getCirclePoints(int numSegments) {
if (numSegments < 3) {
return null;
}
ArrayList<Vec4d> ret = new ArrayList<>();
double thetaStep = 2 * Math.PI / numSegments;
for (int i = 0; i < numSegments + 1; ++i) {
double theta = i * thetaStep;
ret.add(new Vec4d(Math.cos(theta), Math.sin(theta), 0, 1.0d));
}
return ret;
}
/**
* Build up a rounded rectangle (similar to the existing stockpiles). Assumes rounding width-wise
* @param width
* @param height
* @return
*/
public static ArrayList<Vec4d> getRoundedRectPoints(double width, double height, int numSegments) {
ArrayList<Vec4d> ret = new ArrayList<>();
// Create semi circles on the ends
double xScale = 1;
double radius = height/2;
double fociiPoint = width/2 - radius;
// If the width is too small, the focii are at 0, and we scale in the x component of the curvature
if (width < height) {
xScale = width/height;
fociiPoint = 0;
}
double thetaStep = 2 * Math.PI / numSegments;
// +X cap
for (int i = 0; i < numSegments/2 + 1; ++i) {
double theta = i * thetaStep;
ret.add(new Vec4d(xScale*(radius*Math.sin(theta) + fociiPoint), -radius*Math.cos(theta), 0, 1.0d));
}
// -X cap
for (int i = 0; i < numSegments/2 + 1; ++i) {
double theta = i * thetaStep;
ret.add(new Vec4d(xScale*(-radius*Math.sin(theta) - fociiPoint), radius*Math.cos(theta), 0, 1.0d));
}
return ret;
}
/**
* Return a number of points that can draw an arc. This returns pairs for lines (unlike the getCirclePoints() which
* returns points for a line-strip).
* @param radius
* @param center
* @param startAngle
* @param endAngle
* @param numSegments
* @return
*/
public static ArrayList<Vec4d> getArcPoints(double radius, Vec4d center, double startAngle, double endAngle, int numSegments) {
if (numSegments < 3) {
return null;
}
ArrayList<Vec4d> ret = new ArrayList<>();
double thetaStep = (startAngle - endAngle) / numSegments;
for (int i = 0; i < numSegments; ++i) {
double theta0 = i * thetaStep + startAngle;
double theta1 = (i+1) * thetaStep + startAngle;
ret.add(new Vec4d(radius * Math.cos(theta0) + center.x, radius * Math.sin(theta0) + center.y, 0, 1.0d));
ret.add(new Vec4d(radius * Math.cos(theta1) + center.x, radius * Math.sin(theta1) + center.y, 0, 1.0d));
}
return ret;
}
/**
*
* @param cameraInfo
* @param x - x coord in window space
* @param y - y coord in window space
* @param width - window width
* @param height - window height
* @return
*/
public static Ray getPickRayForPosition(CameraInfo cameraInfo, int x, int y, int width, int height) {
double aspectRatio = (double)width / (double)height;
double normX = 2.0*((double)x / (double)width) - 1.0;
double normY = 1.0 - 2.0*((double)y / (double)height); // In openGL space, y is -1 at the bottom
return RenderUtils.getViewRay(cameraInfo, aspectRatio, normX, normY);
}
/**
* Get a Ray representing a line starting at the camera position and projecting through the current mouse pointer
* in this window
* @param mouseInfo
* @return
*/
public static Ray getPickRay(Renderer.WindowMouseInfo mouseInfo) {
if (mouseInfo == null || !mouseInfo.mouseInWindow) {
return null;
}
return getPickRayForPosition(mouseInfo.cameraInfo,
mouseInfo.x,
mouseInfo.y,
mouseInfo.width,
mouseInfo.height);
}
/**
* Get a ray from the camera's point of view
* @param camInfo
* @param aspectRatio
* @param x - normalized [-1. 1] screen x coord
* @param y - normalized [-1. 1] screen y coord
* @return
*/
public static Ray getViewRay(CameraInfo camInfo, double aspectRatio, double x, double y) {
double yScale, xScale;
if (aspectRatio > 1) {
xScale = Math.tan(camInfo.FOV/2);
yScale = xScale / aspectRatio;
} else {
yScale = Math.tan(camInfo.FOV/2);
xScale = yScale * aspectRatio;
}
Vec4d dir = new Vec4d(x * xScale, y * yScale, -1, 0); // This will be normalized by Ray()
Vec4d start = new Vec4d(0, 0, 0, 1.0d);
// Temp is the ray in eye-space
Ray temp = new Ray(start, dir);
// Transform by the camera transform to get to global space
return temp.transform(camInfo.trans);
}
/**
* Return a matrix that is the combination of the transform and non-uniform scale
* @param trans
* @param scale
* @return
*/
public static Mat4d mergeTransAndScale(Transform trans, Vec3d scale) {
Mat4d ret = new Mat4d();
trans.getMat4d(ret);
ret.scaleCols3(scale);
return ret;
}
/**
* Get the inverse (in Matrix4d form) of the combined Transform and non-uniform scale factors
* @param trans
* @param scale
* @return
*/
public static Mat4d getInverseWithScale(Transform trans, Vec3d scale) {
Transform t = new Transform(trans);
t.inverse(t);
Mat4d ret = new Mat4d();
t.getMat4d(ret);
Vec3d s = new Vec3d(scale);
// Prevent dividing by zero
if (s.x == 0) { s.x = 1; }
if (s.y == 0) { s.y = 1; }
if (s.z == 0) { s.z = 1; }
ret.scaleRows3(new Vec3d(1/s.x, 1/s.y, 1/s.z));
return ret;
}
/**
* Scale an awt BufferedImage to a given resolution
* @param img
* @param newWidth
* @param newHeight
* @return a new BufferedImage of the appropriate size
*/
public static BufferedImage scaleToRes(BufferedImage img, int newWidth, int newHeight) {
int oldWidth = img.getWidth();
int oldHeight = img.getHeight();
BufferedImage ret = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB);
AffineTransform at = new AffineTransform();
at.scale((double)newWidth/(double)oldWidth, (double)newHeight/(double)oldHeight);
AffineTransformOp scaleOp =
new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
ret = scaleOp.filter(img, ret);
return ret;
}
// Get the closest point in a line segment to a ray
public static Vec4d rayClosePoint(Mat4d rayMatrix, Vec4d worldA, Vec4d worldB) {
// Create vectors for a and b in ray space
Vec4d a = new Vec4d(0.0d, 0.0d, 0.0d, 1.0d);
a.mult4(rayMatrix, worldA);
Vec4d b = new Vec4d(0.0d, 0.0d, 0.0d, 1.0d);
b.mult4(rayMatrix, worldB);
Vec4d ab = new Vec4d(0.0d, 0.0d, 0.0d, 1.0d); // The line A to B
Vec4d negA = new Vec4d(0.0d, 0.0d, 0.0d, 1.0d); // -1 * A
negA.sub3(a);
ab.sub3(b, a);
double dot = negA.dot2(ab)/ab.magSquare2();
if (dot < 0) {
// The closest point is the A point
return new Vec4d(worldA);
} else if (dot >= 1) {
// B is closest
return new Vec4d(worldB);
} else {
// An intermediate point is closest
Vec4d worldAB = new Vec4d(0.0d, 0.0d, 0.0d, 1.0d);
worldAB.sub3(worldB, worldA);
Vec4d ret = new Vec4d(0.0d, 0.0d, 0.0d, 1.0d);
ret.scale3(dot, worldAB);
ret.add3(worldA);
return ret;
}
}
// Get the angle (in rads) this point is off the ray, this is useful for collision cones
// This will return an negative angle for points behind the start of the ray
public static double angleToRay(Mat4d rayMatrix, Vec4d worldP) {
Vec4d p = new Vec4d(0.0d, 0.0d, 0.0d, 1.0d);
p.mult4(rayMatrix, worldP);
return Math.atan(p.mag2() / p.z);
}
public static Vec4d getGeometricMedian(ArrayList<Vec3d> points) {
assert(points.size() > 0);
double minX = points.get(0).x;
double maxX = points.get(0).x;
double minY = points.get(0).y;
double maxY = points.get(0).y;
double minZ = points.get(0).z;
double maxZ = points.get(0).z;
for (Vec3d p : points) {
if (p.x < minX) minX = p.x;
if (p.x > maxX) maxX = p.x;
if (p.y < minY) minY = p.y;
if (p.y > maxY) maxY = p.y;
if (p.z < minZ) minZ = p.z;
if (p.z > maxZ) maxZ = p.z;
}
return new Vec4d((minX+maxX)/2, (minY+maxY)/2, (minZ+maxZ)/2, 1.0d);
}
public static Vec3d getPlaneCollisionDiff(Plane p, Ray r0, Ray r1) {
double r0Dist = p.collisionDist(r0);
double r1Dist = p.collisionDist(r1);
if (r0Dist < 0 || r0Dist == Double.POSITIVE_INFINITY ||
r1Dist < 0 || r1Dist == Double.POSITIVE_INFINITY)
{
// The plane is parallel or behind one of the rays...
return new Vec4d(0.0d, 0.0d, 0.0d, 1.0d); // Just ignore it for now...
}
// The points where the previous pick ended and current position. Collision is with the entity's XY plane
Vec3d r0Point = r0.getPointAtDist(r0Dist);
Vec3d r1Point = r1.getPointAtDist(r1Dist);
Vec3d ret = new Vec3d();
ret.sub3(r0Point, r1Point);
return ret;
}
/**
* This is scratch space for matrix marshaling
*/
private static final float[] MAT_MARSHAL = new float[16];
/**
* Marshal a Mat4d into a static scratch space, this should only be called by the render thread, but I'm not
* putting a guard here for performance reasons. This returns a colum major float array
* @param mat
* @return
*/
public static float[] MarshalMat4d(Mat4d mat) {
MAT_MARSHAL[ 0] = (float)mat.d00;
MAT_MARSHAL[ 1] = (float)mat.d10;
MAT_MARSHAL[ 2] = (float)mat.d20;
MAT_MARSHAL[ 3] = (float)mat.d30;
MAT_MARSHAL[ 4] = (float)mat.d01;
MAT_MARSHAL[ 5] = (float)mat.d11;
MAT_MARSHAL[ 6] = (float)mat.d21;
MAT_MARSHAL[ 7] = (float)mat.d31;
MAT_MARSHAL[ 8] = (float)mat.d02;
MAT_MARSHAL[ 9] = (float)mat.d12;
MAT_MARSHAL[10] = (float)mat.d22;
MAT_MARSHAL[11] = (float)mat.d32;
MAT_MARSHAL[12] = (float)mat.d03;
MAT_MARSHAL[13] = (float)mat.d13;
MAT_MARSHAL[14] = (float)mat.d23;
MAT_MARSHAL[15] = (float)mat.d33;
return MAT_MARSHAL;
}
/**
* Marshal a 4x4 matrix into 'array', filling the 16 values starting at 'offset'
* @param mat
* @param array
* @param offset
*/
public static void MarshalMat4dToArray(Mat4d mat, float[] array, int offset) {
array[ 0 + offset] = (float)mat.d00;
array[ 1 + offset] = (float)mat.d10;
array[ 2 + offset] = (float)mat.d20;
array[ 3 + offset] = (float)mat.d30;
array[ 4 + offset] = (float)mat.d01;
array[ 5 + offset] = (float)mat.d11;
array[ 6 + offset] = (float)mat.d21;
array[ 7 + offset] = (float)mat.d31;
array[ 8 + offset] = (float)mat.d02;
array[ 9 + offset] = (float)mat.d12;
array[10 + offset] = (float)mat.d22;
array[11 + offset] = (float)mat.d32;
array[12 + offset] = (float)mat.d03;
array[13 + offset] = (float)mat.d13;
array[14 + offset] = (float)mat.d23;
array[15 + offset] = (float)mat.d33;
}
private static final double SMALL_SCALE = 0.000001;
public static Vec3d fixupScale(Vec3d inScale) {
Vec3d ret = new Vec3d(inScale);
if (ret.x <= 0.0) ret.x = SMALL_SCALE;
if (ret.y <= 0.0) ret.y = SMALL_SCALE;
if (ret.z <= 0.0) ret.z = SMALL_SCALE;
return ret;
}
private static boolean isValidExtension(String fileName) {
int idx = fileName.lastIndexOf('.');
if (idx < 0)
return false;
String ext = fileName.substring(idx + 1).trim().toUpperCase();
if (ext.equals("DAE") || ext.equals("OBJ") || ext.equals("JSM") || ext.equals("JSB"))
return true;
return false;
}
public static MeshProtoKey FileNameToMeshProtoKey(URI fileURI) {
try {
URL meshURL = fileURI.toURL();
String fileString = fileURI.toString();
String ext = fileString.substring(fileString.length() - 4,
fileString.length());
if (ext.toUpperCase().equals(".ZIP")) {
// This is a zip, use a zip stream to actually pull out
// the .dae file
ZipInputStream zipInputStream = new ZipInputStream(meshURL.openStream());
// Loop through zipEntries
for (ZipEntry zipEntry; (zipEntry = zipInputStream
.getNextEntry()) != null;) {
String entryName = zipEntry.getName();
if (!isValidExtension(entryName))
continue;
// This zipEntry is a collada file, no need to look
// any further
// Abuse URI a bit to URI-encode the filename
URI encEntryURI = new URI(null, entryName, null);
String encEntry = encEntryURI.getRawSchemeSpecificPart();
meshURL = new URL("jar:" + meshURL + "!/"
+ encEntry);
break;
}
}
MeshProtoKey ret = new MeshProtoKey(meshURL.toURI());
return ret;
} catch (MalformedURLException e) {
LogBox.renderLogException(e);
assert (false);
} catch (IOException e) {
assert (false);
} catch (URISyntaxException e) {
LogBox.renderLogException(e);
assert (false);
}
return null;
}
/**
* Get the difference in Z height from projecting the two rays onto the vertical plane
* defined at the provided centerPoint
* @param centerPoint
* @param currentRay
* @param lastRay
* @return
*/
public static double getZDiff(Vec3d centerPoint, Ray currentRay, Ray lastRay) {
// Create a plane, orthogonal to the camera, but parallel to the Z axis
Vec4d normal = new Vec4d(currentRay.getDirRef());
normal.z = 0;
normal.normalize3();
double planeDist = centerPoint.dot3(normal);
Plane plane = new Plane(normal, planeDist);
double currentDist = plane.collisionDist(currentRay);
double lastDist = plane.collisionDist(lastRay);
if (currentDist < 0 || currentDist == Double.POSITIVE_INFINITY ||
lastDist < 0 || lastDist == Double.POSITIVE_INFINITY)
{
// The plane is parallel or behind one of the rays...
return 0; // Just ignore it for now...
}
// The points where the previous pick ended and current position. Collision is with the entity's XY plane
Vec3d currentPoint = currentRay.getPointAtDist(currentDist);
Vec3d lastPoint = lastRay.getPointAtDist(lastDist);
return currentPoint.z - lastPoint.z;
}
public static int[] stringToCodePoints(String s) {
int numCodePoints = s.codePointCount(0, s.length());
int[] ret = new int[numCodePoints];
for (int i = 0; i < numCodePoints; ++i) {
ret[i] = s.codePointAt(s.offsetByCodePoints(0, i));
}
return ret;
}
}