use of net.drewke.tdme.math.Matrix4x4 in project tdme by andreasdr.
the class DAEReader method readNode.
/**
* Reads a DAE visual scene group node
* @param authoring tool
* @param path name
* @param model
* @param parent group
* @param xml node
* @param xml root
* @param frames per seconds
* @throws Exception
* @return group
*/
private static Group readNode(AuthoringTool authoringTool, String pathName, Model model, Group parentGroup, Element xmlRoot, Element xmlNode, float fps) throws Exception {
String xmlNodeId = xmlNode.getAttribute("id");
String xmlNodeName = xmlNode.getAttribute("name");
if (xmlNodeId.length() == 0)
xmlNodeId = xmlNodeName;
StringTokenizer t = null;
// default node matrix
Matrix4x4 transformationsMatrix = null;
// set up local transformations matrix
List<Element> xmlMatrixElements = getChildrenByTagName(xmlNode, "matrix");
if (xmlMatrixElements.size() == 1) {
String xmlMatrix = getChildrenByTagName(xmlNode, "matrix").get(0).getTextContent();
t = new StringTokenizer(xmlMatrix, " \n\r");
//
transformationsMatrix = new Matrix4x4(Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken())).transpose();
}
// tdme model definitions
Group group = new Group(model, parentGroup, xmlNodeId, xmlNodeName);
//
if (transformationsMatrix != null) {
group.getTransformationsMatrix().multiply(transformationsMatrix);
}
// parse animations
List<Element> xmlAnimationsLibrary = getChildrenByTagName(xmlRoot, "library_animations");
if (xmlAnimationsLibrary.isEmpty() == false) {
List<Element> xmlAnimations = getChildrenByTagName(xmlAnimationsLibrary.get(0), "animation");
for (Element xmlAnimation : xmlAnimations) {
// older DAE has animation/animation xml nodes
List<Element> _xmlAnimation = getChildrenByTagName(xmlAnimation, "animation");
if (_xmlAnimation.isEmpty() == false) {
xmlAnimation = _xmlAnimation.get(0);
}
// find sampler source
String xmlSamplerSource = null;
Element xmlChannel = getChildrenByTagName(xmlAnimation, "channel").get(0);
if (xmlChannel.getAttribute("target").startsWith(xmlNodeId + "/")) {
xmlSamplerSource = xmlChannel.getAttribute("source").substring(1);
}
// check for sampler source
if (xmlSamplerSource == null) {
continue;
}
// parse animation output matrices
String xmlSamplerOutputSource = null;
String xmlSamplerInputSource = null;
Element xmlSampler = getChildrenByTagName(xmlAnimation, "sampler").get(0);
for (Element xmlSamplerInput : getChildrenByTagName(xmlSampler, "input")) {
if (xmlSamplerInput.getAttribute("semantic").equals("OUTPUT")) {
xmlSamplerOutputSource = xmlSamplerInput.getAttribute("source").substring(1);
} else if (xmlSamplerInput.getAttribute("semantic").equals("INPUT")) {
xmlSamplerInputSource = xmlSamplerInput.getAttribute("source").substring(1);
}
}
// check for sampler source
if (xmlSamplerOutputSource == null) {
throw new ModelFileIOException("Could not find xml sampler output source for animation for " + xmlNodeId);
}
// load animation input matrices
// TODO: check accessor "time"
float[] keyFrameTimes = null;
for (Element xmlAnimationSource : getChildrenByTagName(xmlAnimation, "source")) {
if (xmlAnimationSource.getAttribute("id").equals(xmlSamplerInputSource)) {
Element xmlFloatArray = getChildrenByTagName(xmlAnimationSource, "float_array").get(0);
int frames = Integer.parseInt(xmlFloatArray.getAttribute("count"));
String valueString = xmlFloatArray.getTextContent();
int keyFrameIdx = 0;
keyFrameTimes = new float[frames];
t = new StringTokenizer(valueString, " \n\r");
while (t.hasMoreTokens()) {
keyFrameTimes[keyFrameIdx++] = Float.parseFloat(t.nextToken());
}
}
}
// load animation output matrices
// TODO: check accessor "transform"
Matrix4x4[] keyFrameMatrices = null;
for (Element xmlAnimationSource : getChildrenByTagName(xmlAnimation, "source")) {
if (xmlAnimationSource.getAttribute("id").equals(xmlSamplerOutputSource)) {
Element xmlFloatArray = getChildrenByTagName(xmlAnimationSource, "float_array").get(0);
int keyFrames = Integer.parseInt(xmlFloatArray.getAttribute("count")) / 16;
// some models have animations without frames
if (keyFrames > 0) {
String valueString = xmlFloatArray.getTextContent();
t = new StringTokenizer(valueString, " \n\r");
// parse key frame
int keyFrameIdx = 0;
keyFrameMatrices = new Matrix4x4[keyFrames];
while (t.hasMoreTokens()) {
// set animation transformation matrix at frame
keyFrameMatrices[keyFrameIdx] = new Matrix4x4(Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken()), Float.parseFloat(t.nextToken())).transpose();
keyFrameIdx++;
}
}
}
}
// create linear animation by key frame times and key frames
if (keyFrameTimes != null && keyFrameMatrices != null) {
int frames = (int) Math.ceil(keyFrameTimes[keyFrameTimes.length - 1] * fps);
// create default animation
ModelHelper.createDefaultAnimation(model, frames);
//
Animation animation = group.createAnimation(frames);
Matrix4x4[] transformationsMatrices = animation.getTransformationsMatrices();
Matrix4x4 tansformationsMatrixLast = keyFrameMatrices[0];
int keyFrameIdx = 0;
int frameIdx = 0;
float timeStampLast = 0.0f;
for (float keyFrameTime : keyFrameTimes) {
Matrix4x4 transformationsMatrixCurrent = keyFrameMatrices[(keyFrameIdx) % keyFrameMatrices.length];
float timeStamp;
for (timeStamp = timeStampLast; timeStamp < keyFrameTime; timeStamp += 1.0f / fps) {
if (frameIdx >= frames) {
System.out.println("Warning: skipping frame: " + frameIdx);
frameIdx++;
continue;
}
Matrix4x4.interpolateLinear(tansformationsMatrixLast, transformationsMatrixCurrent, (timeStamp - timeStampLast) / (keyFrameTime - timeStampLast), transformationsMatrices[frameIdx]);
frameIdx++;
}
timeStampLast = timeStamp;
tansformationsMatrixLast = transformationsMatrixCurrent;
keyFrameIdx++;
}
}
}
}
// parse sub groups
for (Element _xmlNode : getChildrenByTagName(xmlNode, "node")) {
Group _group = readVisualSceneNode(authoringTool, pathName, model, group, xmlRoot, _xmlNode, fps);
if (_group != null) {
group.getSubGroups().put(_group.getId(), _group);
model.getGroups().put(_group.getId(), _group);
}
}
// check for geometry data
String xmlInstanceGeometryId = null;
List<Element> xmlInstanceGeometryElements = getChildrenByTagName(xmlNode, "instance_geometry");
if (xmlInstanceGeometryElements.isEmpty() == false) {
Element xmlInstanceGeometryElement = xmlInstanceGeometryElements.get(0);
// fetch instance geometry url
xmlInstanceGeometryId = xmlInstanceGeometryElement.getAttribute("url").substring(1);
// determine bound materials
HashMap<String, String> materialSymbols = new HashMap<String, String>();
for (Element xmlBindMaterial : getChildrenByTagName(xmlInstanceGeometryElement, "bind_material")) for (Element xmlTechniqueCommon : getChildrenByTagName(xmlBindMaterial, "technique_common")) for (Element xmlInstanceMaterial : getChildrenByTagName(xmlTechniqueCommon, "instance_material")) {
materialSymbols.put(xmlInstanceMaterial.getAttribute("symbol"), xmlInstanceMaterial.getAttribute("target"));
}
// parse geometry
readGeometry(authoringTool, pathName, model, group, xmlRoot, xmlInstanceGeometryId, materialSymbols);
//
return group;
}
// otherwise check for "instance_node"
String xmlInstanceNodeId = null;
for (Element xmlInstanceNodeElement : getChildrenByTagName(xmlNode, "instance_node")) {
xmlInstanceNodeId = xmlInstanceNodeElement.getAttribute("url").substring(1);
}
// do we have a instance node id?
if (xmlInstanceNodeId != null) {
for (Element xmlLibraryNodes : getChildrenByTagName(xmlRoot, "library_nodes")) for (Element xmlLibraryNode : getChildrenByTagName(xmlLibraryNodes, "node")) if (xmlLibraryNode.getAttribute("id").equals(xmlInstanceNodeId)) {
// parse sub groups
for (Element _xmlNode : getChildrenByTagName(xmlLibraryNode, "node")) {
Group _group = readVisualSceneNode(authoringTool, pathName, model, parentGroup, xmlRoot, _xmlNode, fps);
if (_group != null) {
group.getSubGroups().put(_group.getId(), _group);
model.getGroups().put(_group.getId(), _group);
}
}
// parse geometry
for (Element xmlInstanceGeometry : getChildrenByTagName(xmlLibraryNode, "instance_geometry")) {
String xmlGeometryId = xmlInstanceGeometry.getAttribute("url").substring(1);
// parse material symbols
HashMap<String, String> materialSymbols = new HashMap<String, String>();
for (Element xmlBindMaterial : getChildrenByTagName(xmlInstanceGeometry, "bind_material")) for (Element xmlTechniqueCommon : getChildrenByTagName(xmlBindMaterial, "technique_common")) for (Element xmlInstanceMaterial : getChildrenByTagName(xmlTechniqueCommon, "instance_material")) {
materialSymbols.put(xmlInstanceMaterial.getAttribute("symbol"), xmlInstanceMaterial.getAttribute("target"));
}
// parse geometry
readGeometry(authoringTool, pathName, model, group, xmlRoot, xmlGeometryId, materialSymbols);
}
}
}
//
return group;
}
use of net.drewke.tdme.math.Matrix4x4 in project tdme by andreasdr.
the class Model method computeTransformationsMatrix.
/**
* Computes a transformations matrix at a given frame for a given group id recursivly
* @param groups
* @param parent transformations matrix
* @param frame
* @param group id
* @return group transformations matrix or null
*/
protected Matrix4x4 computeTransformationsMatrix(HashMap<String, Group> groups, Matrix4x4 parentTransformationsMatrix, int frame, String groupId) {
// iterate through groups
for (Group group : groups.getValuesIterator()) {
// group transformation matrix
Matrix4x4 transformationsMatrix = null;
// compute animation matrix if animation setups exist
Animation animation = group.getAnimation();
if (animation != null) {
Matrix4x4[] animationMatrices = animation.getTransformationsMatrices();
transformationsMatrix = animationMatrices[frame % animationMatrices.length].clone();
}
// do we have no animation matrix?
if (transformationsMatrix == null) {
// no animation matrix, set up local transformation matrix up as group matrix
transformationsMatrix = group.getTransformationsMatrix().clone();
} else {
// we have animation matrix, so multiply it with group transformation matrix
transformationsMatrix.multiply(group.getTransformationsMatrix());
}
// apply parent transformation matrix
if (parentTransformationsMatrix != null) {
transformationsMatrix.multiply(parentTransformationsMatrix);
}
// return matrix if group matches
if (group.getId().equals(groupId))
return transformationsMatrix;
// calculate sub groups
HashMap<String, Group> subGroups = group.getSubGroups();
if (subGroups.size() > 0) {
Matrix4x4 tmp = computeTransformationsMatrix(subGroups, transformationsMatrix, frame, groupId);
if (tmp != null)
return tmp;
}
}
//
return null;
}
use of net.drewke.tdme.math.Matrix4x4 in project tdme by andreasdr.
the class Object3DGroupMesh method createMesh.
/**
* Creates a object3d group mesh from group
* @param animation processing target
* @param group
* @param transformationm matrices
* @return object 3d group mesh
*/
protected static Object3DGroupMesh createMesh(Engine.AnimationProcessingTarget animationProcessingTarget, Group group, HashMap<String, Matrix4x4> transformationMatrices) {
Object3DGroupMesh mesh = new Object3DGroupMesh();
// group data
Vector3[] groupVertices = group.getVertices();
Vector3[] groupNormals = group.getNormals();
TextureCoordinate[] groupTextureCoordinates = group.getTextureCoordinates();
Vector3[] groupTangents = group.getTangents();
Vector3[] groupBitangents = group.getBitangents();
// determine face count
int faceCount = group.getFaceCount();
// set up face count
mesh.faces = faceCount;
// animation processing target
mesh.animationProcessingTarget = animationProcessingTarget;
// transformations for skinned meshes
Skinning skinning = group.getSkinning();
mesh.skinning = skinning != null;
// transformed mesh vertices
mesh.transformedVertices = new Vector3[groupVertices.length];
for (int j = 0; j < mesh.transformedVertices.length; j++) {
mesh.transformedVertices[j] = new Vector3().set(groupVertices[j]);
}
// transformed mesh normals
mesh.transformedNormals = new Vector3[groupNormals.length];
for (int j = 0; j < mesh.transformedNormals.length; j++) {
mesh.transformedNormals[j] = new Vector3().set(groupNormals[j]);
}
// texture coordinates
if (groupTextureCoordinates != null) {
mesh.textureCoordinates = new TextureCoordinate[groupTextureCoordinates.length];
for (int j = 0; j < mesh.textureCoordinates.length; j++) {
mesh.textureCoordinates[j] = new TextureCoordinate(groupTextureCoordinates[j]);
}
}
// transformed mesh tangents
if (groupTangents != null) {
mesh.transformedTangents = new Vector3[groupTangents.length];
for (int j = 0; j < mesh.transformedTangents.length; j++) {
mesh.transformedTangents[j] = new Vector3().set(groupTangents[j]);
}
}
// transformed mesh bitangents
if (groupBitangents != null) {
mesh.transformedBitangents = new Vector3[groupBitangents.length];
for (int j = 0; j < mesh.transformedBitangents.length; j++) {
mesh.transformedBitangents[j] = new Vector3().set(groupBitangents[j]);
}
}
// indices
int indicesCount = 0;
for (FacesEntity facesEntity : group.getFacesEntities()) {
indicesCount += 3 * facesEntity.getFaces().length;
}
mesh.indices = new short[indicesCount];
{
int j = 0;
// create face vertex indices
for (FacesEntity facesEntity : group.getFacesEntities()) for (Face face : facesEntity.getFaces()) for (int vertexIndex : face.getVertexIndices()) {
mesh.indices[j++] = (short) vertexIndex;
}
}
//
mesh.recreatedBuffers = false;
// create mesh upload buffers
if (mesh.animationProcessingTarget != Engine.AnimationProcessingTarget.CPU_NORENDERING) {
mesh.sbIndices = ByteBuffer.allocateDirect(mesh.faces * 3 * Short.SIZE / Byte.SIZE).order(ByteOrder.nativeOrder()).asShortBuffer();
mesh.fbVertices = ByteBuffer.allocateDirect(groupVertices.length * 3 * Float.SIZE / Byte.SIZE).order(ByteOrder.nativeOrder()).asFloatBuffer();
mesh.fbNormals = ByteBuffer.allocateDirect(groupNormals.length * 3 * Float.SIZE / Byte.SIZE).order(ByteOrder.nativeOrder()).asFloatBuffer();
mesh.fbTextureCoordinates = groupTextureCoordinates != null ? ByteBuffer.allocateDirect(groupTextureCoordinates.length * 2 * Float.SIZE / Byte.SIZE).order(ByteOrder.nativeOrder()).asFloatBuffer() : null;
mesh.fbTangents = groupTangents != null ? ByteBuffer.allocateDirect(groupTangents.length * 3 * Float.SIZE / Byte.SIZE).order(ByteOrder.nativeOrder()).asFloatBuffer() : null;
mesh.fbBitangents = groupBitangents != null ? ByteBuffer.allocateDirect(groupBitangents.length * 3 * Float.SIZE / Byte.SIZE).order(ByteOrder.nativeOrder()).asFloatBuffer() : null;
// create face vertex indices, will never be changed in engine
for (FacesEntity facesEntity : group.getFacesEntities()) for (Face face : facesEntity.getFaces()) for (int vertexIndex : face.getVertexIndices()) {
mesh.sbIndices.put((short) vertexIndex);
}
mesh.sbIndices.flip();
// create texture coordinates buffer, will never be changed in engine
if (mesh.fbTextureCoordinates != null) {
// construct texture coordinates byte buffer as this will not change usually
for (TextureCoordinate textureCoordinate : groupTextureCoordinates) {
mesh.fbTextureCoordinates.put(textureCoordinate.getArray());
}
mesh.fbTextureCoordinates.flip();
}
}
// group transformations matrix
if (mesh.animationProcessingTarget == Engine.AnimationProcessingTarget.CPU || mesh.animationProcessingTarget == Engine.AnimationProcessingTarget.CPU_NORENDERING) {
// group transformations matrix
mesh.cGroupTransformationsMatrix = transformationMatrices.get(group.getId());
}
// skinning
if (skinning != null) {
// skinning computation caches if computing skinning on CPU
if (mesh.animationProcessingTarget == Engine.AnimationProcessingTarget.CPU || mesh.animationProcessingTarget == Engine.AnimationProcessingTarget.CPU_NORENDERING) {
mesh.cSkinningJointWeight = new float[groupVertices.length][];
mesh.cSkinningJointBindMatrices = new Matrix4x4[groupVertices.length][];
mesh.cSkinningJointTransformationsMatrices = new Matrix4x4[groupVertices.length][];
mesh.cTransformationsMatrix = new Matrix4x4();
// compute joint weight caches
Joint[] joints = skinning.getJoints();
float[] weights = skinning.getWeights();
JointWeight[][] jointsWeights = skinning.getVerticesJointsWeights();
for (int vertexIndex = 0; vertexIndex < groupVertices.length; vertexIndex++) {
int vertexJointWeights = jointsWeights[vertexIndex].length;
if (vertexJointWeights > mesh.cSkinningMaxVertexWeights)
mesh.cSkinningMaxVertexWeights = vertexJointWeights;
mesh.cSkinningJointWeight[vertexIndex] = new float[vertexJointWeights];
mesh.cSkinningJointBindMatrices[vertexIndex] = new Matrix4x4[vertexJointWeights];
mesh.cSkinningJointTransformationsMatrices[vertexIndex] = new Matrix4x4[vertexJointWeights];
int jointWeightIdx = 0;
for (JointWeight jointWeight : jointsWeights[vertexIndex]) {
Joint joint = joints[jointWeight.getJointIndex()];
//
mesh.cSkinningJointWeight[vertexIndex][jointWeightIdx] = weights[jointWeight.getWeightIndex()];
mesh.cSkinningJointBindMatrices[vertexIndex][jointWeightIdx] = joint.getBindMatrix();
mesh.cSkinningJointTransformationsMatrices[vertexIndex][jointWeightIdx] = transformationMatrices.get(joint.getGroupId());
// next
jointWeightIdx++;
}
}
} else // GPU setup
if (mesh.animationProcessingTarget == Engine.AnimationProcessingTarget.GPU) {
// create skinning buffers
mesh.gIbSkinningVerticesJoints = ByteBuffer.allocateDirect(groupVertices.length * 1 * Float.SIZE / Byte.SIZE).order(ByteOrder.nativeOrder()).asFloatBuffer();
mesh.gFbSkinningVerticesVertexJointsIdxs = ByteBuffer.allocateDirect(groupVertices.length * 4 * Float.SIZE / Byte.SIZE).order(ByteOrder.nativeOrder()).asFloatBuffer();
mesh.gFbSkinningVerticesVertexJointsWeights = ByteBuffer.allocateDirect(groupVertices.length * 4 * Float.SIZE / Byte.SIZE).order(ByteOrder.nativeOrder()).asFloatBuffer();
mesh.gFbSkinningJointsTransformationsMatrices = ByteBuffer.allocateDirect(60 * 16 * Float.SIZE / Byte.SIZE).order(ByteOrder.nativeOrder()).asFloatBuffer();
mesh.gFbSkinningTransformationMatrix = new Matrix4x4();
// fill skinning buffers, joint bind matrices
mesh.skinningJoints = skinning.getJoints().length;
mesh.gSkinningJointBindMatrices = new ArrayList<Matrix4x4>();
for (Joint joint : skinning.getJoints()) {
mesh.gSkinningJointBindMatrices.add(joint.getBindMatrix());
}
//
JointWeight[][] jointsWeights = skinning.getVerticesJointsWeights();
float[] weights = skinning.getWeights();
for (int groupVertexIndex = 0; groupVertexIndex < groupVertices.length; groupVertexIndex++) {
int vertexJoints = jointsWeights[groupVertexIndex].length;
// put number of joints
mesh.gIbSkinningVerticesJoints.put((float) vertexJoints);
// vertex joint idx 1..4
for (int i = 0; i < 4; i++) {
mesh.gFbSkinningVerticesVertexJointsIdxs.put((float) (vertexJoints > i ? jointsWeights[groupVertexIndex][i].getJointIndex() : -1));
}
// vertex joint weight 1..4
for (int i = 0; i < 4; i++) {
mesh.gFbSkinningVerticesVertexJointsWeights.put(vertexJoints > i ? weights[jointsWeights[groupVertexIndex][i].getWeightIndex()] : 0.0f);
}
}
// put number of joints
mesh.gIbSkinningVerticesJoints.flip();
// vertex joint idx 1..4
mesh.gFbSkinningVerticesVertexJointsIdxs.flip();
// vertex joint weight 1..4
mesh.gFbSkinningVerticesVertexJointsWeights.flip();
}
}
// temp vector3
mesh.tmpVector3 = new Vector3();
// issue a recreate buffer and upload to graphics board
mesh.recreateBuffers(group);
//
return mesh;
}
use of net.drewke.tdme.math.Matrix4x4 in project tdme by andreasdr.
the class CircleParticleEmitter method fromTransformations.
/*
* (non-Javadoc)
* @see net.drewke.tdme.engine.subsystems.particlesystem.ParticleEmitter#fromTransformations(net.drewke.tdme.engine.Transformations)
*/
public void fromTransformations(Transformations transformations) {
Matrix4x4 transformationsMatrix = transformations.getTransformationsMatrix();
// apply rotation, scale, translation
transformationsMatrix.multiply(center, centerTransformed);
// apply transformations rotation + scale to axis
transformationsMatrix.multiplyNoTranslation(axis0, axis0Transformed);
transformationsMatrix.multiplyNoTranslation(axis1, axis1Transformed);
// note:
// sphere radius can only be scaled the same on all axes
// thats why its enough to only take x axis to determine scaling
side.set(axis0).scale(radius).add(center);
transformationsMatrix.multiply(side, side);
radius = side.sub(center).computeLength();
}
use of net.drewke.tdme.math.Matrix4x4 in project tdme by andreasdr.
the class CircleParticleEmitterPlaneVelocity method fromTransformations.
/*
* (non-Javadoc)
* @see net.drewke.tdme.engine.subsystems.particlesystem.ParticleEmitter#fromTransformations(net.drewke.tdme.engine.Transformations)
*/
public void fromTransformations(Transformations transformations) {
Matrix4x4 transformationsMatrix = transformations.getTransformationsMatrix();
// apply rotation, scale, translation
transformationsMatrix.multiply(center, centerTransformed);
// apply transformations rotation + scale to axis
transformationsMatrix.multiplyNoTranslation(axis0, axis0Transformed);
transformationsMatrix.multiplyNoTranslation(axis1, axis1Transformed);
// note:
// sphere radius can only be scaled the same on all axes
// thats why its enough to only take x axis to determine scaling
side.set(axis0).scale(radius).add(center);
transformationsMatrix.multiply(side, side);
radius = side.sub(center).computeLength();
}
Aggregations